站在巨人肩上操作 CAS(一):CAS 的原理
在某个线程执行完时间片之后,就会进行 CPU 切换;切换过程:清空寄存器和缓存数据,重新加载新的线程所需要的数据(线程私有的:JVM 运行时所需的数据:程序计数器、虚拟机栈和本地方法栈);
此时之前的线程就被挂起,加入到阻塞队列中,在一定的时间和条件下,通过调用 notify() 或 notifyAll() 唤醒,进入就绪状态(runna
ble),等待 CPU 调度。
总结:
总是假设最坏的情况,并且只有在确保其他线程不会造成干扰的情况下才会执行;
导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁,再次进行锁的竞争(公平锁 / 非公平锁)。
缺点
线程会被频繁的挂起,唤醒之后需要重新抢占锁
场景:如果一个线程需要某个资源,但是这个资源的 占用时间很短 ,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,但是可能立刻就发现资源已被刚才的线程的释放,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高。
乐观锁
思路
每次 不加锁 而是假设 没有冲突 而去完成某种 操作,如果因为冲突 失败就重试 ,直到成功为止;
当某个线程不让出 CPU 时,当前线程就一直 while 循环去尝试获取锁,如果失败就重试,直到成功为止;
适用场景:当数据争用不严重时,乐观锁效果更好。
乐观锁应用
CAS
非阻塞算法
核心参数
主内存中存放的 V 值:线程共享(对应 JVM 的共享内存区:堆和方法区)
线程上次从内存中读取的 V 值 A:线程私有,存放在线程的栈帧中
需要写入内存中并改写 V 值的 B:线程对 A 值进行操作之后的值,想要存入主内存 V 中
V 值在主内存中保存,所有线程在对 V 值操作时,需要先从主存中读取 V 值到线程到工作内存 A 中;
然后对 A 值操作之后,把结果保存在 B 中,最后再把 B 值赋给主内存的 V;
多线程共用 V 值都是这样的过程;
核心点:将 B 值赋给 V 之前,先比较 A 值和 V 值是否相等(判断,当前线程在计算过程中,是否有其他线程已经修改了 V 值),不相等表示此时的 V 值已经被其他线程改变,需要重新执行上面的过程;相等就将 B 值赋给 V。
如果没有 CAS 这样的机制,那在多线程的场景下,两个线程同时对共享数据进行修改,很容易就出错,导致某一个线程的修改被忽略。
核心代码
//原子操作
if (A == V) {
V = B;
return B;
} else {
return V;
}
变量 V
如何保证线程每次操作 V 时,都去主内存中获取 V 值?
自然是在 CAS 的实现代码中,变量 V 是用 volatile 原语 修饰的:保证其可见性;
如何保证在进行 V 和 A 值比较相等之后,而在 B 值赋给 V 之前,V 值不会被其他线程所修改?
也就是说如何保证比较和替换这两个步骤的原子性?
看一下 CAS 原理:
CAS 原理
底层的实现其实时通过调用 JNI(Java Native Interface 本地调用)的代码实现的;(其实 JNI 就是为了支持 Java 去调用其他语言)
具体看一下:AtomicInteger 类的 compareAndSet() 方法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
调用的时 sun.misc 包下 Unsafe 类的 compareAndSwapInt()方法:(本地方法)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
本地方法会调用 JDK 中的几个 cpp 文件(这是在 widowsX86 中的,不同操作系统调用的文件不同,文件中的实现方式也不同);
具体的 C++代码我就不贴了,知道上层的实现思路就行;
底层大致思路:通过对 cmpxchg 指令添加 lock 前缀(windowsX86 中的实现),确保对主内存中 V 值的 读-改-写操作原子执行;
不同的操作系统有不同的实现方式,但大致的思路是类似的,目的也都是保证比较赋值操作是原子操作。
更详细的底层实现,以及相关的一些关于 CPU 锁的内容,可以去看一下《Java 并发编程艺术》。
最终的实现作用:
确保对主内存中 V 值的 读-改-写操作原子执行;
禁止该指令与之前和之后的读(比较)和写(赋值)指令重排序。
ABA 问题
问题描述
CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决方案
版本号
ABA 问题的解决思路就是使用版本号;
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A
AtomicStampedReference
compareAndSet() 方法源码:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
跟其他的 Atomic 类不同的是, 这个多了一个引用的比较;
这个会先检查 当前引用是否等于预期引用 ,并且当前标志是否等于预期标志,如果全部相等,则以 原子方式 将该 引用 和该标志的 值 设置为给定的更新值;
所以如果有其他线程修改了, 那么引用就会改变, 当前线程就执行失败重试操作;
循环时间长
问题描述
因为 CAS 机制是: 一直进行失败重试, 直到成功为止, 那么自旋 CAS 如果长时间不成功, 就会给 CPU 带来非常大的执行开销;
评论