资讯专栏INFORMATION COLUMN

我们常说的 CAS 自旋锁是什么

cloud / 661人阅读

摘要:,即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。在多线程环境下,原子操作是保证线程安全的重要手段。如下面这个例子,个线程,每个线程都执行次操作,我们期望的值是,但是很遗憾,结果总是小于的。

CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。

它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 true。否则,返回 false。

保证原子操作

任何技术的出现都是为了解决某些特定的问题, CAS 要解决的问题就是保证原子操作。原子操作是什么,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,知道操作完成。在多线程环境下,原子操作是保证线程安全的重要手段。举个例子来说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对一个整数 i 进行自增操作,需要基本的三个步骤:

1、读取 i 的当前值;

2、对 i 值进行加 1 操作;

3、将 i 值写回内存;

假设两个进程都读取了 i 的当前值,假设是 0,这时候 A 线程对 i 加 1 了,B 线程也 加 1,最后 i 的是 1 ,而不是 2。这就是因为自增操作不是原子操作,分成的这三个步骤可以被干扰。如下面这个例子,10个线程,每个线程都执行 10000 次 i++ 操作,我们期望的值是 100,000,但是很遗憾,结果总是小于 100,000 的。

      
    static int i = 0;
    
    public static void add(){
        i++;
    }
    
    private static class Plus implements Runnable{

        @Override
        public void run(){
            for(int k = 0;k<10000;k++){
                add();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread[] threads = new Thread[10];
        for(int i = 0;i<10;i++){
            threads[i] = new Thread(new Plus());
            threads[i].start();
        }

        for(int i = 0;i<10;i++){
            threads[i].join();
        }
        System.out.println(i);
    }

既然这样,那怎么办。没错,也许你已经想到了,可以加锁或者利用 synchronized 实现,例如,将 add() 方法修改为如下这样:

public synchronized static void add(){
        i++;
    }

或者,加锁操作,例如下面使用 ReentrantLock (可重入锁)实现。

private static Lock lock = new ReentrantLock();
    public static void add(){
        lock.lock();
        i++;
        lock.unlock();
    }
CAS 实现自旋锁

既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。

上面也说了,CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。

其实 JDK 中有好多地方用到了 CAS ,尤其是 java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到过 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。

这里拿 AtomicBoolean 来举个例子,因为它足够简单。

public class AtomicBoolean implements java.io.Serializable {
    private static final long serialVersionUID = 4654671469794556979L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    
    public final boolean get() {
        return value != 0;
    }

    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
   
  }

这是 AtomicBoolean 的部分代码,我们看到这里面又几个关键方法和属性。

1、使用了 sun.misc.Unsafe 对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用;

2、value 表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下 volatile 可见性问题,欢迎关注

3、valueOffset 是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;

4、compareAndSet 方法,这就是实现 CAS 的核心方法了,在使用 AtomicBoolean 的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。

使用场景

CAS 适合简单对象的操作,比如布尔值、整型值等;

CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;

比如 AtomicBoolean 可以用在这样一个场景下,系统需要根据一个布尔变量的状态属性来判断是否需要执行一些初始化操作,如果是多线程的环境下,避免多次重复执行,可以使用 AtomicBoolean 来实现,伪代码如下:

private final static AtomicBoolean flag = new AtomicBoolean();
    if(flag.compareAndSet(false,true)){
        init();
    }

比如 AtomicInteger 可以用在计数器中,多线程环境中,保证计数准确。

ABA问题

CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。

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

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

相关文章

  • Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    摘要:公平锁非公平锁公平锁公平锁是指多个线程按照申请锁的顺序来获取锁。加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下: 公平锁 / 非公平锁 可重入锁 / 不可重入锁 独享锁 / 共享锁 互斥锁 / 读...

    LeoHsiun 评论0 收藏0
  • 不可不说的Java“锁”事

    摘要:本文旨在对锁相关源码本文中的源码来自使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。中,关键字和的实现类都是悲观锁。自适应意味着自旋的时间次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 前言 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景...

    galaxy_robot 评论0 收藏0
  • Java中的锁以及sychronized实现机制

    摘要:有可能,会造成优先级反转或者饥饿现象。悲观锁在中的使用,就是利用各种锁。对于而言,其是独享锁。偏向锁,顾名思义,它会偏向于第一个访问锁的线程,大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。 理解锁的基础知识 如果想要透彻的理解java锁的来龙去脉,需要先了解以下基础知识。 基础知识之一:锁的类型 按照其性质分类 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获...

    linkin 评论0 收藏0
  • java并发机制与底层实现原理

    摘要:并发机制与底层实现原理是轻量级的它在多处理器开发中保证了共享变量的可见性,因为它不会引起线程上下文的切换和调度,所以比的使用和执行成本更底。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。 java并发机制与底层实现原理 volatile volatile是轻量级的synchronize,它在多处理器开发中保证了共享变量的可见性,因为它...

    scola666 评论0 收藏0
  • 且听我一个故事讲透一个锁原理之synchronized

    摘要:第三天,太监传话钦天监求见一日无事。第四天,钦天监一日无事。然后所有的竞争线程放弃自旋,逐个插入到对象里的一个队列尾部,进入阻塞状态。 微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。 showImg(https://segmentfault.com/img/bVbrgsJ?w=900...

    gougoujiang 评论0 收藏0

发表评论

0条评论

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