资讯专栏INFORMATION COLUMN

CAS 算法 —— Compare and Swap

mmy123456 / 1307人阅读

摘要:算法算法会先对一个内存变量位置和一个给定的值进行比较,如果相等,则用一个新值去修改这个内存变量位置。因为是非公平锁,所以一上来就尝试抢占锁给定旧值并希望用新值去更新内存变量。

本文翻译和原创各占一半,所以还是厚颜无耻归类到原创好了...
https://howtodoinjava.com/jav...
java 5 其中一个令人振奋的改进是新增了支持原子操作的类型,例如 AtomicInteger, AtomicLong 等。在多线程环境中进行简单的自增自减操作时,这些原子类能帮助你减少很多用于多线程同步的复杂代码。这些原子类依赖于 CAS (compare and swap) 算法,接下来我们会讨论 CAS 这个概念。

乐观锁和悲观锁

传统的锁机制,例如 java 的 synchronized 关键字,他代表了 java 中悲观锁技术,保证了某一时刻仅有一个线程能访问同步代码/方法。synchronized 能够很好地工作,却有着 (相对) 比较大的性能开销。
乐观锁 (相对悲观锁) 对性能会有很大的帮助。他的核心思想是:你寄希望于在没有冲突的情况下完成一次更新操作,使用乐观锁技术更新时会进行 “冲突检测” 来判断是否有其他的线程干扰,若是 (有其他线程干扰) 则视本次更新操作失败,一般会进行重试 (可以了解一下CAS自旋)。Compare and Swap 就是典型的乐观锁技术。

CAS 算法

CAS 算法会先对一个内存变量(位置) V 和一个给定的值进行比较 A ,如果相等,则用一个新值 B 去修改这个内存变量(位置)。上述过程会作为一个原子操作完成 (intel处理器通过 cmpxchg 指令系列实现)。CAS 原子性保证了新值的计算是基于上一个有效值,期间如果内存变量(位置) V 被其他线程更新了,本线程的 CAS 更新操作将会失败。CAS 操作必须告诉调用者成功与否,可以返回一个 boolean 值来表示,或者返回一个从内存变量读到的值 (应该是上一次有效值)

CAS 操作数有三个:

内存变量(位置) V,表示被更新的变量

线程上一次读到的旧值 A

用来覆盖 V 的新值 B

CAS 表示:“我认为现在 V 的值还是之前我读到的旧值 A,若是则用新值 B 覆盖内存变量 V,否则不做任何动作并告诉调用者操作失败”。CAS 是一项乐观锁技术,他在更新的时候总是希望能成功 (没有冲突),但也能检测出来自其他线程的冲突和干扰
Java 中的 Compare and Swap

这里我们关注一下ReentrantLock锁定和解锁那部分的源码

//ReentrantLock.lock()
public void lock() {
    sync.lock();
}

他依赖了其内部类Synclock(),以下是内部类 Sync (继承了队列同步器 AQS)

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    abstract void lock();
    ................

Sync还是个抽象类,一般 new ReentrantLock() 时创建的是 NonfairSync

// ReentrantLock的构造方法
public ReentrantLock() {
    sync = new NonfairSync();
}

下面就是NonfairSynclock() 方法了

final void lock() {
    if (compareAndSetState(0, 1)) // 1
        setExclusiveOwnerThread(Thread.currentThread()); // 2
    else
        acquire(1); // 3
}

1 中的 compareAndSetState() 承继自队列同步器 AQS,封装了 CAS 指令。因为是 NonfairSync 非公平锁,所以一上来就尝试抢占锁:给定旧值 0 并希望用新值 1 去更新内存变量 State。若更新成功则视为获取锁成功,并执行 2

2 成功完成了 CAS 操作 (没错,当你使用 CAS 指令成功把 State 从 0 更新成 1 便视为获取锁,就是这么简单粗暴 ╮(╯▽╰)╭ ),把当前线程设为独占线程

3 操作失败 (被人抢先获取锁(╯`□′)╯╧╧),进行 acquire 操作再次尝试获取锁,若还是不行,则把当前线程加入 AQS 等待队列,由 AQS 来管理队列中等待线程的阻塞和唤醒,具体代码就不贴出来了,AQS 的源码多处使用到 CAS 指令,有兴趣的同学可以查看

锁用完了要释放,下面贴出 unlock() 方法

// ReentrantLock.unlock()
public void unlock() {
    sync.release(1);
}

这里还是依赖了 sync,release() 是 AQS 的通用方法,其内部调用了 tryRelease() (由 Sync 类实现),这里直接贴出 Sync 的 tryRelease()

protected final boolean tryRelease(int releases) { // releases 参数的值是上面传进来的 1
    int c = getState() - releases; // 1
    if (Thread.currentThread() != getExclusiveOwnerThread()) // 1.5
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { // 2
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c); // 3
    return free;
}

1 处的c 是内存变量 State 即将要被更新的值,因为 ReentrantLock 是可重入锁 (当前线程可多次获取锁),所以 State 的值是可以大于 1 的。

2 判断若新值为 0,则视为锁被释放并设置当前独占线程为 null

3 把 State 的值更新为 c,思考一下这里的更新操作为什么没用到 CAS 指令?

1.5 解释了上面的疑问,只有当前独占线程有能力对 State 变量进行修改,不需要进行同步或使用 CAS

Summary

AQS 队列同步器以及 java.util.concurrent 下各种锁和原子类都运用到的 CAS 算法,有时间的同学建议阅读加深印象。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73081.html

相关文章

  • 猫头鹰的深夜翻译:Java中的CAS(Compare And Swap)

    摘要:否则它就会用新的值替代当前值。在这种情况下,锁可能会优于原子变量,但在实际的争用级别中,原子变量的性能优于锁。在中引入了另外一个构件。 题目要求 在我们深入了解CAS(Compare And Swap)策略以及它是如何在AtomicInteger这样的原子构造器中使用的,首先来看一下这段代码: public class MyApp { private volatile int ...

    hosition 评论0 收藏0
  • java 锁机制

    摘要:在包中已经包含了读写锁乐观锁总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或操作实现。 重入锁 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好...

    wfc_666 评论0 收藏0
  • CAS

    摘要:与同步方式在多线程情况下,可以采用同步阻塞悲观锁或乐观锁的方式实现业务,具体要看业务场景,如果重试的代价很小,那用是合适的,但如果每次重试都需要花费大量的时间或资源,那应该采用同步方式。 CAS介绍 CAS - Compare And Swap (Compare And Set, Check And Set) wikipedia的描述如下: 比较并交换(compare and swap...

    joy968 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<