CAS操作
概念
是Compare-And-Swap的缩写,即比较交换。是一种用于多线程编程的原子指令, 底层实现原理依赖于硬件的支持, 通常是通过处理器提供的特定指令来实现的。常用于实现无锁编程,通过他实现了原子操作,保证并发的安全性,不会造成所谓的数据不一致性的问题。
操作涉及的三个操作数
- 内存位置(V):要操作的变量,其实就是主内存中要操作的变量
- 预期原值(A):预期变量当前的值,其实就是线程的工作内存中变量副本
- 新值(B):如果变量的当前值与预期的原值相同,需要写入的新值。
操作的执行逻辑
- 首先会检车主内存位置V的当前值是否与预期原值A相等
- 如果相等,说明自从这个值被读取以来没有被其他线程修改过,操作就会将新值B写入到这个内存位置
- 如果不相等,说明有其他线程已经修改了这个变量,操作不会执行任何写操作
执行的示例
- 线程1从主内存中读取count变量的值到自己的工作内存中,此时count为10。(这个值我们记为:线程1期望的count值、最初读取到的count值)
- 线程1对自己工作内存中的count的值进行自增操作,此时count为11。(这个值我们记为:线程1更新之后的count值)
- 这个时候,当线程1还未执行到第三步更新count的值到主内存中的时候,线程2突然抢到了CPU执行权,它进来从主内存中读取count变量的值到自己的工作内存中,因为线程1还未更新,所以此时count仍为10。(这个值我们记为:线程2期望的count值、最初读取到的count值)
- 线程2对自己工作内存中的count的值进行自增操作,此时count为11。(这个值我们记为:线程2更新之后的count值)
- 在线程2要将自己工作内存中的count值更新到主内存之前,要执行一部重要的操作!!!就是再次读取主内存中共享变量count的值,如果此时读取到的这个值与最开始线程2期望的count值相等,那么就顺利更新count的值;如果不相等,就撤销放弃本次count++操作。
- 那么线程2最开始期望的count值为10,而此时从主内存中读取到的count值还是10,一样,所以就更新,将自己工作内存中的count值更新到主内存中,此时count为11。
- 那么线程2执行完了,此时线程1重新获得CPU执行权,它已经完成了count++的前两步了,就剩最后一步更新操作了,那么它此时同线程2一样,再次从主内存中读取共享变量count的值,一样就更新、不一样就撤销放弃操作。然而count的值已经被线程2更新过了,此时为11,而线程1最开始期望的count的值为10,两次读取到count的值不一样了!!!所以线程1就会自动撤销本次count++操作。
- 最终,本次线程1、2对count变量执行++操作的结果:线程1失败、线程2成功,最后count的值只增加了1次,为11
操作系统是如何保证CAS操作在回写期间组织其他线程访问的
总线锁
- 所谓的总线索就是使用处理器提供的一份LOCK指令,当一个处理器在总线上输出此信号时,其他处理器的亲故去将被阻塞住,那么该处理器可以独占使用共享内存。
- 在早期和一些低级硬件架构中,处理器可能会使用总线锁定来确保对内存访问的独占权。总线锁定通过阻止其他所有处理器访问内存系统来确保正在执行的 CAS 操作不会遇到并发冲突。当一个处理器在执行原子操作时,它会发出一个信号锁定总线,直到操作完成为止。这种方法简单但效率不高,因为它阻止了所有其他处理器的内存操作,影响了系统的整体性能。
缓存锁
在现代多核处理器中,通过缓存一致性协议(如 MESI 协议)来管理各个核心的缓存行状态,从而维护一致性和原子性。
- 获取缓存行:
处理器将目标内存地址对应的缓存行加载到它的 L1 缓存中(如果缓存行不在缓存中或者不是最新的)。 - 锁定缓存行:
在支持缓存锁的架构中,处理器会对这个缓存行加锁。这通常不是锁定总线,而是在缓存层面实现的锁。
在支持 MESI 协议的系统中,处理器通过将缓存行置于 Exclusive(独占)状态来隐式地“锁定”这个缓存行。当缓存行处于 Exclusive 状态时,表明没有其他处理器缓存了这个地址的副本。 - 执行 CAS 操作:
处理器比较缓存中的值和提供的预期值。如果相符,则更新为新值。
这一比较和交换操作在硬件级别是原子执行的。 - 释放缓存行:
在操作完成后,如果使用了缓存行锁定,则锁会被释放。
在 MESI 协议中,如果缓存行内容被修改,则它的状态可能会转变为 Modified,并通过缓存一致性机制通知其他核心这一变化。
CAS的优缺点
优点
- 避免锁的开销:
CAS 提供了一种无锁的同步机制,避免了传统锁(如互斥锁和读写锁)带来的开销和复杂度。它不涉及锁管理的开销,如上下文切换、调度延迟和死锁。 - 系统吞吐率提高:
在多处理器系统中,CAS 可以减少线程阻塞的时间,提高系统整体的并发能力和吞吐率。 - 死锁风险降低:
由于 CAS 是无锁的,它不会导致传统锁可能引起的死锁情况。 - 实现简单的原子操作:
对于简单的数据结构,如计数器、标志和指针,使用 CAS 可以简单而有效地实现它们的线程安全操作。
缺点
- 循环重试的开销:
在高争用的环境中,多个线程可能反复尝试更新同一变量,导致高失败率和多次重试。这种情况下,CAS 的性能可能会下降,因为每次失败后,线程都需要重新加载变量,再尝试更新。 - ABA问题:
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
引用此文章
1.实际上这种假设不一定总是成立的。假如说有共享变量 count=100。
2.A线程将 count 修改为 200
3.B线程将 count 修改为 300
4.C线程将 count 修改为 100
5.当前线程看到的count变量的值是100,这就与最初的count变量的值是一样的,那么是否就认为count变量的值没有被其他线程更新呢?显然不是啊,它明显的被A、B两个线程更新过。
6.这就是CAS中的ABA问题,即共享变量经历了 A → B → A 的更新。
如果想要规避ABA问题,可以为共享变量引入一个修订好(或者叫时间戳),每次修改共享变量时,相应的修订号就会增加1,ABA问题的过程就转变为:[A,0] → [B,1] → [A,2] ,每次修改共享变量都会导致修订号的增加,通过修订号就可以准确的判断共享便是否被其他线程修改过。在原子类中,AtomicReference就会面临ABA问题的困扰,而AtomicStampedReference 可以很好的规避ABA问题。详细内容看下面的代码案例。