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 {
- @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 一定比加锁性能要高吗?
来源: https://www.cnblogs.com/liqiangchn/p/11902198.html