目录
1.CAS介绍
CAS全称为CompareAndSwap,直译为“比较并替换”。其实CAS的应用场景非常多,但实际在开发很难感知到,更多的做为一种思想或底层实现封装起来。常见的如:乐观锁、volatile、AtomicInteger、AtomicBoolean...、JUC包的并发实现。
2.应用场景
假设需要对一个data变量做 100 次“++”操作交由 2 个线程执行,结果会是什么。
public class CasTest {
private static int num = 2;
private static ExecutorService executor = Executors.newFixedThreadPool(num);
private static int data = 0;
public static void main(String[] args) throws InterruptedException {
CasTest casTest = new CasTest();
for (int i = 0; i < 100; i++) {
executor.execute(casTest::increment);
}
// 休眠1秒等待线程执行
Thread.sleep(1000);
System.out.println("data:" + data);
executor.shutdown();
}
private void increment() {
data++;
}
}
结果的范围为2~100,为什么可以看看这篇文章的分析 。很明显与期望的100有很大出入。因为并发的对data变量修改,在读取、操作、替换这三个步骤发生了线程不安全的问题。因此出现结果不准确。
3.使用 synchronized
public class CasTest {
private static int num = 2;
private static ExecutorService executor = Executors.newFixedThreadPool(num);
private static int data = 0;
public static void main(String[] args) throws InterruptedException {
CasTest casTest = new CasTest();
for (int i = 0; i < 100; i++) {
executor.execute(casTest::increment);
}
// 休眠1秒等待线程执行
Thread.sleep(1000);
System.out.println("data:" + data);
executor.shutdown();
}
private synchronized void increment() {
data++;
}
}
3.1 分析 synchronized
通过上述修改,可以发现data的数值在多线程运行下始终为100。线程安全。但synchronized到底做了什么?程序在编译后都会转为指令,而synchronized在方法体指令执行的前后分别加上 monitorenter 和 monitorexit 这两个字节码指令。
在执行 monitorenter 指令时,首先要尝试获取对象锁。如果这个对象没被锁定或者当前线程已经占有锁,则锁的计数器加 1 ;再执行 monitorexit 指令时会将锁计数器减 1 ,当计数器为0时。锁就被释放。如果获取锁失败,则线程阻塞。直至对象锁被释放。不难想象使用了 synchronized 是的线程调用 increment 方法都变为串行化执行。
设想 increment 方法中还需要做的很多,但这些操作并不涉及线程安全问题。synchronized迫使整个方法串行化,可想而知操作效率并不高。
4 使用 AtomicInteger
public class CasTest {
private static int num = 2;
private static ExecutorService executor = Executors.newFixedThreadPool(num);
private static AtomicInteger data = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
CasTest casTest = new CasTest();
for (int i = 0; i < 100; i++) {
executor.execute(casTest::increment);
}
// 休眠1秒等待线程执行
Thread.sleep(2000);
System.out.println("data:" + data);
executor.shutdown();
}
private void increment() {
// ....
data.incrementAndGet();
// ....
}
}
代码中之所以不去掉 increment 方法是想表达,方法内可能还有额外的操作。但就当前这个场景。完全可以讲同步把控的范围缩小为data变量自身。这里使用的 AtomicInteger 是jdk并发包中的Atomic原子类。应对多线程并发修改。而 Atomic 线程安全主要依赖于的底层就是使用的 CAS 机制。
4.1 CAS机制
场景:假设三个线程同时对 data 做 incrementAndGet 操作,CAS是如何解决冲突问题?
过程梳理:
(1)线程1 获取data值为0。
(2)线程1 执行CAS。先比较data是否等于0,是所以放心的修改为1。
(3)线程2、线程3 同时获取data值为1。
(4)线程2 率先执行完加 1操作。执行CAS。此时data等于修改前获取的值,所以放心修改为2。
(5)线程3 执行完加 1操作。执行CAS。此时发现data已经不是修改前获取的值。CAS失败。
(6)线程3 重新执行业务。再次获取data值为2。
(7)线程3 执行完加 1操作。执行CAS。此时data等于修改前获取的值,所以放心修改为3。
(8)至此业务结束,data在多线程操作下安全的修改为 3。
4.2 与 synchronized 比较
本质上各有各的应用场景,synchronized也做了很多优化如 偏向锁、轻量级锁、重量级锁。性能有了很大提升。但就多线程修改data这个业务场景。针对的是变量而非方法。故并不需要锁机制,将一些无线程安全问题的操作都一并纳入同步代码中。而CAS更切合这个场景下变量同步的使用。
4.3 CAS缺点
(1)ABA问题
CAS在修改时需要确认修改值是否有改动。但有一种情况是值原来是A。
步骤一:线程1 获取后修改为B但尚未执行CAS。
步骤二:此时线程2 修改为B,并完成CAS。
步骤三:线程3 再次修改B为A,并完成CAS。
步骤是:回到线程1 在执行CAS 发现值还是A,就赋值为B。
但实质上A已经不是初始条件的A。在某些情景似乎不影响业务,例如上述的data++。但如果使用data记录某种原子操作。此时就有问题。因为ABA导致操作非原子。
(2)多线程自旋
如果多线程执行CAS由于业务耗时原因或者其他原因,导致CAS频繁失败。此时系统处理任务的效率会非常低。CPU资源消耗严重。都用在了不断重试。
(3)无法同时保证多个共享变量的原子性
当对一个共享变量执行操作时,可以使用CAS保证原子操作。但涉及多个共享变量操作时,CAS就无法保证操作的原子性。此时可以使用锁机制。也可以使用AtomicRefrence保证应用对象之间的原子性。设置一个对象属性为共享变量。
4.4 CAS改进
针对CAS缺点中的“多线程自旋”,设想下如果一个线程维护一个变量是不是就没有这种问题。但这就不是共享变量了。另一种想法就是是否可以将共享变量做拆分。类似 ConcurrentHashMap “分段锁” 原理。修改时只要确保线程分到的段未被修改。减少 “自旋”的可能性,从而提升性能。
在JDK8中就有 LngAdder 、DoubleAdder 其原理就是 “分段CAS” 和 “自动分段迁移”。
分段CAS: 内部会维护一个Cell数组,数值的每一个索引为一个分段记录则值一部分。
自动分段迁移: Cell的值在执行CAS失败后会自动切换到另一个Cell分段执行CAS操作。
5 总结
CAS在某种情景固然是提高了并发操作的性能。减少了上锁的问题。但CAS也有其自身的缺点。任何技术都不是“一劳永逸”的。具体问题还要具体分析。