并发编程-硬件加持的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亿次累加运算,然后打印出平均耗时。

Copy
/** * cas对比加锁测试 * * @author Jann Lee * @date 2019-11-21 0:12 **/ public class CasTest { @Test public 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一定比加锁性能要高吗?

posted @   Java填坑笔记  阅读(772)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
点击右上角即可分享
微信分享提示
CONTENTS