1.面试必问的码部CAS,你懂了吗?
2.C#中使用CAS实现无锁算法
3.ListenableFuture源码解析
4.从HotSpot源码,码部深度解读 park 和 unpark
5.Linux基础组件之无锁消息队列ypipe/yqueue详解
6.ReentrantLock 源码解析 | 京东云技术团队
面试必问的码部CAS,你懂了吗?
CAS(Compare-and-Swap)是码部一种实现并发算法时常用的技术,Java并发包中的码部多个类采用了CAS技术。面试中经常涉及这一概念,码部小夜灯源码本文旨在深入解析CAS的码部原理。
在介绍CAS之前,码部我们通过一个例子来理解其应用。码部这个例子在运行过程中可能会陷入死循环,码部通过检查线程状态,码部发现IDEA监控线程的码部介入导致了问题。解决这一死循环的码部方法是使用DEBUG模式运行程序或调整代码逻辑。
通过volatile关键字的码部使用,我们可以观察到,码部其只能确保可见性,而不能保证原子性。在并发场景下,对于自增操作等非原子操作,volatile并不能保证正确结果。因此,解决这类问题的关键是引入原子操作。
引入synchronized关键字是一种常见的解决方法,通过加锁实现原子操作,确保每次操作的正确性。然而,频繁使用synchronized会导致性能下降,因此引入Java并发包中的原子操作类(如AtomicInteger)成为了更优选择。
AtomicInteger的`getAndIncrement()`方法即是CAS操作的实例,它通过一系列原子操作确保每次自增操作的正确性和性能。进一步分析,我们发现其底层实现调用了`compareAndSwapInt`方法,即CAS的核心实现。
CAS(Compare-and-Swap)本质上是一个比较并替换操作,它需要三个操作数:内存地址、旧的预期值和目标值。执行过程中,当内存地址的值与预期值相等时,CAS尝试将内存地址的值修改为目标值。若失败,则获取最新值,源码时代测试面试重新尝试,直至修改成功。
深入到源码层面,可以看到`AtomicInteger`类中的`getAndIncrement()`方法最终调用了`compareAndSwapInt`方法,而`compareAndSwapInt`在Unsafe类中被实现。通过调用`Atomic::cmpxchg`方法,我们能够看到具体的汇编指令实现。
`Atomic::cmpxchg`方法的实现依赖于系统是否为多处理器环境,以优化性能。它通过`LOCK_IF_MP`宏决定是否添加`lock`前缀,这一优化措施在单处理器环境下通常没有必要,但在多核处理器中能够提升性能。
CAS操作的缺点包括循环时间长导致的开销大、仅支持单一共享变量的原子操作,以及ABA问题。ABA问题是由于CAS操作的特性导致,即在读取值后,值在后续操作中可能被改回原始值,从而产生误判。为解决ABA问题,Java并发包提供了带有版本控制的原子引用类`AtomicStampedReference`。
在使用CAS操作时,需要考虑其对并发正确性的影响,尤其是ABA问题。如果并发场景中存在可能的ABA问题,传统互斥同步可能比原子类更高效。
综上所述,理解CAS的原理和其在并发编程中的应用对于深入学习Java并发技术至关重要。掌握CAS操作的原理、优缺点及其解决方法,将有助于更高效、正确地处理并发问题。
C#中使用CAS实现无锁算法
CAS(Compare-and-Swap)操作是一种多线程并发编程中常用的原子操作,用于实现多线程间的同步和互斥访问。它通过比较内存地址处的值与期望的旧值是否相等来实现这一目标。若相等,则将新值写入该内存地址;否则不做任何操作。CAS 操作的原子性由硬件层面的CPU指令保证,通常通过 Interlocked 类在 C# 中实现。
在C#中,投注源码站我们使用Interlocked类的CompareExchange方法来实现CAS操作。此方法接收三个参数:内存地址、期望的旧值和新值。如果内存地址处的值与期望的旧值相等,则将新值写入该内存地址并返回旧值;否则不执行任何操作。通过判断返回值与期望的旧值是否相等,我们可以得知CompareExchange操作是否成功。
在使用CAS实现无锁算法时,我们通常需要在更新数据后执行进一步的操作。结合while(true)循环,我们可以在每次尝试更新数据后检查是否成功。如果失败,则继续尝试,直到成功为止。
以下是一个简单的计数器示例,它使用CAS实现了一个线程安全的自增操作。在CLR底层源码中,我们也经常看到使用类似方法实现线程安全计数器的代码。同时,队列类也使用CAS实现线程安全的入队和出队操作,该操作更为复杂,需要不断检查是否有其他线程修改数据。
在复杂的无锁算法中,每一步操作都必须考虑是否被其他线程修改。每一步操作非原子,因此我们不仅依赖CAS操作,还必须确保在执行每个操作前检查数据是否被修改。类比薛定谔的猫,我们不知道数据状态直到尝试修改时才确定。
通过测试代码,我们可以观察到在一定数量的操作中,需要重试的次数。这个重试次数取决于队列中是否有数据可供操作,而在多线程环境下,每次操作的结果可能有所不同。
CAS是一种乐观锁机制,假设数据未被其他线程修改,若未修改则直接修改,若已修改则重新获取数据并再次尝试修改。在实现复杂的朔源码查源头数据结构时,我们不仅依赖CAS操作,还需注意数据是否被其他线程修改,以及处理可能的分支情况。
ListenableFuture源码解析
ListenableFuture 是 spring 中对 JDK Future 接口的扩展,主要应用于解决在提交线程池的任务拿到 Future 后在 get 方法调用时会阻塞的问题。通过使用 ListenableFuture,可以向其注册回调函数(监听器),当任务完成时,触发回调。Promise 在 Netty 中也实现了类似的功能,用于处理类似 Future 的场景。
实现 ListenableFuture 的关键在于 FutureTask 的源码解析。FutureTask 是实现 Future 接口的基础类,ListenableFutureTask 在其基础上做了扩展。其主要功能是在任务提交后,当调用 get 方法时能够阻塞当前业务线程,直到任务完成时唤醒。
FutureTask 通过在内部实现一个轻量级的 Treiber stack 数据结构来管理等待任务完成的线程。这个数据结构由 WaitNode 节点组成,每个节点代表一个等待的线程。当业务线程调用 get 方法时,会将自己插入到 WaitNode 栈中,并且在插入的同时让当前线程进入等待状态。在任务执行完成后,会遍历 WaitNode 栈,唤醒等待的线程。
为了确保并发安全,FutureTask 使用 CAS(Compare and Swap)操作来管理 WaitNode 栈。每个新插入的节点都会使用 CAS 操作与栈顶节点进行比较,并在满足条件时更新栈顶。这一过程保证了插入操作的原子性,防止了并发条件下的数据混乱。同时,插入操作与栈顶节点的更新操作相互交织,确保了数据的一致性和完整性。
在 FutureTask 中,还利用了 LockSupport 类提供的 park 和 unpark 方法来实现线程的等待和唤醒。当线程插入到 WaitNode 栈中后,通过 park 方法将线程阻塞;任务执行完成后,通过 unpark 方法唤醒线程,博客网站源码模板完成等待与唤醒的流程。
综上所述,ListenableFuture 通过扩展 FutureTask 的功能,实现了任务执行与线程等待的高效管理。通过注册监听器并利用 CAS 操作与 LockSupport 方法,实现了在任务完成时通知回调,解决了异步任务执行时的线程阻塞问题,提高了程序的并发处理能力。
从HotSpot源码,深度解读 park 和 unpark
我最近建立了一个在线自习室(App:番茄ToDO)用于相互监督学习,感兴趣的小伙伴可以加入。自习室加入码:D5A7A
Java并发包下的类大多基于AQS(AbstractQueuedSynchronizer)框架实现,而AQS线程安全的实现依赖于两个关键类:Unsafe和LockSupport。
其中,Unsafe主要提供CAS操作(关于CAS,在文章《读懂AtomicInteger源码(多线程专题)》中讲解过),LockSupport主要提供park/unpark操作。实际上,park/unpark操作的最终调用还是基于Unsafe类,因此Unsafe类才是核心。
Unsafe类的实现是由native关键字说明的,这意味着这个方法是原生函数,是用C/C++语言实现的,并被编译成了DLL,由Java去调用。
park函数的作用是将当前调用线程阻塞,而unpark函数则是唤醒指定线程。
park是等待一个许可,unpark是为某线程提供一个许可。如果线程A调用park,除非另一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。每次调用一次park,需要有一个unpark来解锁。
并且,unpark可以先于park调用,但不管unpark先调用多少次,都只提供一个许可,不可叠加。只需要一次park来消费掉unpark带来的许可,再次调用会阻塞。
在Linux系统下,park和unpark是通过Posix线程库pthread中的mutex(互斥量)和condition(条件变量)来实现的。
简单来说,mutex和condition保护了一个叫_counter的信号量。当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。当_counter=0时线程阻塞,当_counter>0时直接设为0并返回。
每个Java线程都有一个Parker实例,Parker类的部分源码如下:
由源码可知,Parker类继承于PlatformParker,实际上是用Posix的mutex和condition来实现的。Parker类里的_counter字段,就是用来记录park和unpark是否需要阻塞的标识。
具体的执行逻辑已经用注释标记在代码中,简要来说,就是检查_counter是不是大于0,如果是,则把_counter设置为0,返回。如果等于零,继续执行,阻塞等待。
unpark直接设置_counter为1,再unlock mutex返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程。源码如下:
(如果不会下载JVM源码可以后台回复“jdk”,获得下载压缩包)
Linux基础组件之无锁消息队列ypipe/yqueue详解
CAS定义
比较并交换(compare and swap, CAS),在多线程编程中用于实现不被打断的数据交换,避免数据不一致问题。该操作通过比较内存值与指定数据,当数值相同则替换内存数据。
为什么需要无锁队列
锁引起的问题:cache损坏/失效、同步机制上的争抢、动态内存分配。
有锁导致线程切换引发cache损坏
大量线程切换导致cache数据失效,处理器与主存之间数据传输效率下降,影响性能。
在同步机制上的争抢队列
阻塞队列导致任务暂停或睡眠,大量时间浪费在获取互斥锁,而非处理数据,引发严重争用。
动态内存分配
多线程中动态分配内存导致互斥,线程频繁分配内存影响应用性能。
无锁队列的实现
无锁队列由ypipe_t和yqueue_t类构成,适用于一读一写场景。通过chunk模式批量分配结点,减少动态内存分配的互斥问题。批量分配大小根据业务场景调整,通常设置较大较为安全。利用spare_chunk存储未释放的chunk,降低频繁分配释放。预写机制减少CAS调用。巧妙的唤醒机制,读端等待无数据时进入等待状态,写端根据返回值判断队列是否为空以唤醒读端。
无锁队列使用
yqueue.write(count,false)用于写入元素并标记完成状态,yqueue.flush()使读端可见更新后数据。yqueue.read(&value)读取元素,返回true表示读到元素,返回false表示队列为空。
ypipe_t使用
write(val, false)更新写入位置,flush()刷新数据到管道,read()读取数据并更新可读位置。
yqueue_t构造函数
初始化队列,end_chunk总是指向最后分配的chunk,back_chunk仅在有元素插入时指向对应的chunk。
front()和back()函数
返回队列头和尾的可读写元素位置。
push()和pop()函数
push()更新写入位置,pop()更新读取位置并检测释放chunk,保持数据流。
源码分析
yqueue_t内部使用chunk批量分配,减少内存操作,spare_chunk存储释放的chunk以供再次使用。ypipe_t构建单写单读无锁队列,通过CAS操作控制读写位置,实现高效数据交换。
ypipe_t / yqueue_t无锁队列利用chunk机制避免频繁内存动态分配,提升性能。通过局部性原理复用回收的chunk,减少资源消耗。flush()检测队列状态通知唤醒,优化数据交换过程。
ReentrantLock 源码解析 | 京东云技术团队
并发指同一时间内进行了多个线程。并发问题是多个线程对同一资源进行操作时产生的问题。通过加锁可以解决并发问题,ReentrantLock 是锁的一种。
1 ReentrantLock
1.1 定义
ReentrantLock 是 Lock 接口的实现类,可以手动的对某一段进行加锁。ReentrantLock 可重入锁,具有可重入性,并且支持可中断锁。其内部对锁的控制有两种实现,一种为公平锁,另一种为非公平锁.
1.2 实现原理
ReentrantLock 的实现原理为 volatile+CAS。想要说明 volatile 和 CAS 首先要说明 JMM。
1.2.1 JMM
JMM (java 内存模型 Java Memory Model 简称 JMM) 本身是一个抽象的概念,并不在内存中真实存在的,它描述的是一组规范或者规则,通过这组规范定义了程序中各个变量的访问方式.
由于 JMM 运行的程序的实体是线程。而每个线程创建时 JMM 都会为其创建一个自己的工作内存 (栈空间), 工作内存是每个线程的私有数据区域。而 java 内存模型中规定所有的变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程的变量的操作 (读取赋值等) 必须在自己的工作内存中去进行,首先要将变量从主存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量操作完后的新值写回主内存,不能直接操作主内存的变量,各个线程的工作内存中存储着主内存的变量拷贝的副本,因不同的线程间无法访问对方的工作内存,线程间的通信必须在主内存来完成。
如图所示:线程 A 对变量 A 的操作,只能是从主内存中拷贝到线程中,再写回到主内存中。
1.2.2 volatile
volatile 是 JAVA 的关键字用于修饰变量,是 java 虚拟机的轻量同步机制,volatile 不能保证原子性。 作用:
作用:CAS 会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读 - 改 - 写操作。
1.2.4 AQSAQS 的全称是 AbstractQueuedSynchronizer(抽象的队列式的同步器),AQS 定义了一套多线程访问共享资源的同步器框架。
AQS 主要包含两部分内容:共享资源和等待队列。AQS 底层已经对这两部分内容提供了很多方法。
2 源码解析
ReentrantLock 在包 java.util.concurrent.locks 下,实现 Lock 接口。
2.1 lock 方法
lock 分为公平锁和非公平锁。
公平锁:
非公平锁:上来先尝试将 state 从 0 修改为 1,如果成功,代表获取锁资源。如果没有成功,调用 acquire。state 是 AQS 中的一个由 volatile 修饰的 int 类型变量,多个线程会通过 CAS 的方式修改 state,在并发情况下,只会有一个线程成功的修改 state。
2.2 acquire 方法
acquire 是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法。
2.3 tryAcquire 方法
tryAcquire 分为公平和非公平两种。
公平:
非公平:
2.4 addWaiter 方法
在获取锁资源失败后,需要将当前线程封装为 Node 对象,并且插入到 AQS 队列的末尾。
2.5 acquireQueued 方法
2.6 unlock 方法
释放锁资源,将 state 减 1, 如果 state 减为 0 了,唤醒在队列中排队的 Node。
3 使用实例
3.1 公平锁
1. 代码:
2. 执行结果:
3. 小结:
公平锁可以保证每个线程获取锁的机会是相等的。
3.2 非公平锁
1. 代码:
2. 执行结果:
3. 小结:
非公平锁每个线程获取锁的机会是随机的。
3.3 忽略重复操作
1. 代码:
2. 执行结果:
3. 小结:
当线程持有锁时,不会重复执行,可以用来防止定时任务重复执行或者页面事件多次触发时不会重复触发。
3.4 超时不执行
1. 代码:
2. 执行结果:
3. 小结:
超时不执行可以防止由于资源处理不当长时间占用资源产生的死锁问题。
4 总结
并发是现在软件系统不可避免的问题,ReentrantLock 是可重入的独占锁,比起 synchronized 功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等,是处理并发问题很好的解决方案。
Spring Cloud + Spring Boot + Mybatis + Uniapp 企业架构之CAS SSO单点登录框架
了解单点登录(SSO)是一种在多个相关应用之间共享认证信息的机制。SSO主要特点在于使用统一的登录入口,通过Web协议(如HTTPS)实现应用之间的认证。单点登录体系包括用户、Web应用和SSO认证中心三个角色。SSO实现基于三个核心原则:所有登录操作在SSO认证中心执行;认证中心通过方法告知Web应用访问用户是否已通过认证;SSO认证中心与所有Web应用建立信任关系。
CAS SSO单点登录体系由CAS Server和CAS Client组成。CAS Server负责用户信息认证,独立部署,处理凭证(Credentials)。CAS Client部署在客户端,当有对本地Web应用受保护资源的访问请求时,重定向到CAS Server进行认证。
采用CAS SSO单点登录无需依赖Cookie实现跨域,优势明显。在不同站点间无需重新登录,即便站点域名不同。传统方案可能遇到Cookie跨域问题,解决复杂。CAS SSO单点登录的官方文档和源代码可直接访问:apereo.org/projects/cas。推荐下载稳定版本,如4.2.1,避免使用最新版本可能存在的不稳定情况。通过远程Maven库下载cas-server-webapp的war包,步骤包括访问Maven地址搜索并下载最新版本。下载cas-client-core的jar包,支持Spring MVC开发,适用于多种协议与开发语言。CAS架构图展示了其组件与工作流程,根据项目需求选择合适的版本进行部署。