什么是 CAS
(1)CAS(compare and swap) 比较并替换, 比较和替换是线程并发算法时用到的一种技术
(2)CAS 是原子操作, 保证并发安全, 而不是保证并发同步
(3)CAS 是 CPU 的一个指令
(4)CAS 是非阻塞的轻量级的乐观锁
为什么说 CAS 是乐观锁
乐观锁, 严格来说并不是锁, 通过原子性来保证数据的同步, 比如说数据库的乐观锁, 通过版本控制来实现等, 所以 CAS 不会保证线程同步乐观的认为在数据更新期间没有其他线程影响
CAS 原理
CAS(compare and swap) 比较并替换, 就是将内存值更新为需要的值, 但是有个条件, 内存值必须与期望值相同举个例子, 期望值 E 内存值 M 更新值 U, 当 E == M 的时候将 M 更新为 U
CAS 应用
由于 CAS 是 CPU 指令, 我们只能通过 JNI 与操作系统交互, 关于 CAS 的方法都在 sun.misc 包下 Unsafe 的类里 java.util.concurrent.atomic 包下的原子类等通过 CAS 来实现原子操作
CAS 举例
- /**
- * Created by Dell on 2018/2/6.
- */
- public class CasLock {
- private static final CountDownLatch latch = new CountDownLatch(5);
- private static AtomicInteger i = new AtomicInteger(0);
- private static int p = 0;
- public static void main(String[] args) throws InterruptedException {
- long time = System.currentTimeMillis();
- ExecutorService pool = Executors.newFixedThreadPool(5);
- for (int j = 0; j < 5; j++) {
- pool.execute(new Runnable() {
- public void run() {
- for (int k = 0; k < 10000; k++) {
- p++; // 不是原子操作
- i.getAndIncrement(); // 调用原子类加 1
- }
- latch.countDown();
- }
- });
- }
- latch.await(); // 保证所有子线程执行完成
- System.out.println(System.currentTimeMillis() - time);
- System.out.println("p=" + p);
- System.out.println("i=" + i);
- pool.shutdown();
- }
- }
输出结果
- "C:\Program Files\Java\jdk1.8.0_91\bin\java" ...
- 8
- p=43204// 结果不正确
- i=50000
- Process finished with exit code 0
根据结果我们发现, 由于多线程异步进行 p++ 操作, 导致结果不正确
为什么 p++ 的记过不正确呢? 比如两个线程读到 p 的值为 1, 然后做加 1 操作, 这时候 p 的值是 2, 而不是 3 而变量 i 的结果却是对的, 这就要归功于 CAS, 下面我们具体看一下原子类
CAS 指令和具体源代码
原子类例如 AtomicInteger 里的方法都很简单, 大家看一看都能懂, 我们具体看下 getAndIncrement 方法下面贴出代码
- // 该方法功能是 Interger 类型加 1
- public final int getAndIncrement() {
- // 主要看这个 getAndAddInt 方法
- return unsafe.getAndAddInt(this, valueOffset, 1);
- }
- //var1 是 this 指针
- //var2 是地址偏移量
- //var4 是自增的数值, 是自增 1 还是自增 N
- public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5;
- do {
- // 获取内存值, 这是内存值已经是旧的, 假设我们称作期望值 E
- var5 = this.getIntVolatile(var1, var2);
- //compareAndSwapInt 方法是重点,
- //var5 是期望值, var5 + var4 是要更新的值
- // 这个操作就是调用 CAS 的 JNI, 每个线程将自己内存里的内存值 M
- // 与 var5 期望值 E 作比较, 如果相同将内存值 M 更新为 var5 + var4, 否则做自旋操作
- } while (! this . compareAndSwapInt ( var1 , var2 , var5 , var5 + var4 ));
- return var5;
- }
解释一下 getAndAddInt 方法的流程
假设有以下情景
1.AB 两个线程
2.jvm 主内存的值 1,AB 工作内存的值为 1(工作内存会拷贝一份主内存的值)
3. 当前期望值为 1, 做加 1 操作
4. 此时 var5 = 1, var4 = 1,
(1)A 线程将 var5 与工作内存值 M 比较, 比较 var5 是否等于 1
(2) 如果相同则将工作内存值修改为 var5+var4 既修改为 2 并同步到主内存, 此时 this 指针里, 示例变量 value 的值就是 2, 结束循环
(3) 如果不相同则其 B 线程修改了主内存的值, 说明 B 线程已经先于 A 线程做了加 1 操作, A 线程没有更新成功需要继续循环, 注意此时 var5 更新为新的内存值, 假设当前的内存值是 2, 那么此时 var5 = 2, var5 + var4 = 3, 重复上述步骤直到成功
下面是 compareAndSwapInt 本地方法的源码, 可以看到使用 cmpxchg 指令实现 CAS, 在效率上有不错的表现
- UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
- UnsafeWrapper("Unsafe_CompareAndSwapInt");
- oop p = JNIHandles::resolve(obj);
- jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
- return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
- UNSAFE_END
CAS 优缺点
优点
非阻塞的轻量级的乐观锁, 通过 CPU 指令实现, 在资源竞争不激烈的情况下性能高, 相比 synchronized 重量锁, synchronized 会进行比较复杂的加锁, 解锁和唤醒操作
缺点
(1)ABA 问题 线程 CD, 线程 D 将 A 修改为 B 后又修改为 A, 此时 C 线程以为 A 没有改变过, java 的原子类 AtomicStampedReference, 通过控制变量值的版本来保证 CAS 的正确性
(2) 自旋时间过长, 消耗 CPU 资源, 如果资源竞争激烈, 多线程自旋长时间消耗资源
CAS 总结
CAS 不仅是乐观锁, 是种思想, 我们也可以在日常项目中通过类似 CAS 的操作保证数据安全, 但并不是所有场合都适合, 曾看过帖子说, 能用 synchronized 就不要用 CAS, 除非遇到性能瓶颈, 因为 CAS 会让代码可读性变差, 这句话看大家怎么理解了
来源: https://juejin.im/post/5a803e61f265da4e914b5b63