无锁并发的CAS为何如此优秀?
Talk is cheap
CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论位置V的值是否等于A, 都将返回V原有的值。
CAS的含义是”我认为V的值应该是A,如果是,那我将V的值更新为B,否则不修改并告诉V的值实际是多少“
Show you my code
在单线程环境中分别使用无锁,加锁以及cas进行十组5亿次累加运算,然后打印出平均耗时。
/*** cas对比加锁测试** @author Jann Lee* @date 2019-11-21 0:12**/ public class CasTest {@Testpublic void test() {long times = 500_000_000;// 记录耗时List<Long> elapsedTime4NoLock = new ArrayList<>(10);List<Long> elapsedTime4Synchronized = new ArrayList<>(10);List<Long> elapsedTime4ReentrantLock = new ArrayList<>(10);List<Long> elapsedTime4Cas = new ArrayList<>(10);// 进行10组试验for (int j = 0; j < 10; j++) {// 无锁long startTime = System.currentTimeMillis();for (long i = 0; i < times; i++) {}long endTime = System.currentTimeMillis();elapsedTime4NoLock.add(endTime - startTime);// synchronized 关键字(隐式锁)startTime = endTime;for (long i = 0; i < times; ) {i = addWithSynchronized(i);}endTime = System.currentTimeMillis();elapsedTime4Synchronized.add(endTime - startTime);// ReentrantLock 显式锁startTime = endTime;ReentrantLock lock = new ReentrantLock();for (long i = 0; i < times; ) {i = addWithReentrantLock(i, lock);}endTime = System.currentTimeMillis();elapsedTime4ReentrantLock.add(endTime - startTime);// cas(AtomicLong底层是用cas实现)startTime = endTime;AtomicLong atomicLong = new AtomicLong();while (atomicLong.getAndIncrement() < times) {}endTime = System.currentTimeMillis();elapsedTime4Cas.add(endTime - startTime);}System.out.println("无锁计算耗时: " + average(elapsedTime4NoLock) + "ms");System.out.println("synchronized计算耗时: " + average(elapsedTime4Synchronized) + "ms");System.out.println("ReentrantLock计算耗时: " + average(elapsedTime4ReentrantLock) + "ms");System.out.println("cas计算耗时: " + average(elapsedTime4Cas) + "ms");}/*** synchronized加锁*/private synchronized long addWithSynchronized(long i) {i = i + 1;return i;}/*** ReentrantLock加锁*/private long addWithReentrantLock(long i, Lock lock) {lock.lock();i = i + 1;lock.unlock();return i;}/*** 计算平均耗时*/private double average(Collection<Long> collection) {return collection.stream().mapToLong(i -> i).average().orElse(0);} }从案例中我们可能看出在单线程环境场景下cas的性能要高于锁相关的操作。当然,在竞争比较激烈的情况下性能可能会有所下降,因为要不断的重试和回退或者放弃操作,这也是CAS的一个缺点所在,因为这些重试,回退等操作通常用开发者来实现。
CAS的实现并非是简单的代码层面控制的,而是需要硬件的支持,因此在不同的体系架构之间执行的性能差异很大。但是一个很管用的经验法则是:在大多数处理器上,在无竞争的锁获取和释放的”快速代码路径“上的开销,大约是CAS开销的两倍。
为何CAS如此优秀
硬件加持,现代大多数处理器都从硬件层面通过一些列指令实现CompareAndSwap(比较并交换)同步原语,进而使操作系统和JVM可以直接使用这些指令实现锁和并发的数据结构。我们可以简单认为,CAS是将比较和交换合成是一个原子操作。
JVM对CAS的支持, 由于Java程序运行在JVM上,所以应对不同的硬件体系架构的处理则需要JVM来实现。在不支持CAS操作的硬件上,jvm将使用自旋锁来实现。
CAS的ABA问题
cas操作让我们减少了锁带来的性能损耗,同时也给我们带来了新的麻烦-ABA问题。
在线程A读取到x的值与执行CAS操作期间,线程B对x执行了两次修改,x的值从100变成200,然后再从200变回100;而后在线程A执行CAS操作过程中并未发现x发生过变化,成功修改了x的值。由于x的值100 ->200->100,所以称之为ABA的原因。
魔高一尺道高一丈,解决ABA的问题目前最常用的办法就是给数据加上“版本号”,每次修改数据时同时改变版本号即可。
Q&A
在竞争比较激烈的情况下,CAS要进行回退,重试等操作才能得到正确的结果,那么CAS一定比加锁性能要高吗?
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
总结
以上是生活随笔为你收集整理的无锁并发的CAS为何如此优秀?的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 八点建议写出优雅的 Java 代码
- 下一篇: NYOJ 298 点的变换(矩阵快速幂)