侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

volatile与CAS原理

林贤钦
2020-05-24 / 0 评论 / 12 点赞 / 643 阅读 / 0 字
温馨提示:
本文最后更新于 2020-07-15,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

volatile与CAS原理

1、volatile关键字

volatile 关键字用于修饰共享可变变量,即没有使用 final 关键字修饰的实例变量或静态变量,相应的变量就被称为 volatile 变量。

1.1、volatile经典使用

典型的DCL双重锁校验单例模型

public class Singleton{
    private volatile static Singleton INSTANCE ;
    private Singleton{}
    public static Singleton getInstance(){
        if(INSTANCE ==null){
            synchronized(Singleton.class){
                if(INSTANCE ==null)
                    INSTANCE  = new Singleton;
            }
        }
        return INSTANCE ;
    }
}

以上的实现特点

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

解释

  • 第一个if语句,是保证在创建完对象后,以后使用这个对象,不用进入synchronized语句块,被锁住,造成性能浪费
  • 第二个if语句,是保证,在锁住状态下,读到的对象还是没创建的,避免指令交错,出现对象重复创建

1.2、volatile的原理

在JMM模型中,每个线程都对应着一个工作线程,主内存中的变量都会复制一份到每个线程的自己的工作空间,线程对变量的操作都在自己的工作内存中,操作完成后再将变量更新至主内存

在单线程情况下,这个肯定是没问题的,当在多线程的情况下就不一定,假如多个线程同时对一个共有变量进行操作,但一个线程对一个共有变量进行读写操作,还没来得及写进去,另外一个线程就读了,造成数据的不一致

所以volatile的作用就来了,使用volatile关键字修饰共有变量

  • 修改volatile变量时会强制将修改后的值刷新的主内存中。

  • 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。

    因此,再读取该变量值的时候就需要重新从读取主内存中的值。

volatile的作用

  1. 保证变量的内存可⻅性

    内存可⻅性,指的是线程之间的可⻅性,当⼀个线程修改了共享变量时,另⼀个线程可以读取到这个修改后的值

  2. 禁⽌volatile变量与普通变量重排序(Java 5 开始才有这个“增强“的volatile内存语义)

    为优化程序性能,对原有的指令执⾏顺序进⾏优化重新排序。

    重排序可能发⽣在多个阶段,⽐如编译重排序、CPU重排序等。

底层实现原理是内存屏障

内存屏障简单来说就是处理器的一组指令,用于实现对内存操作的顺序限制。

  1. 对 volatile 变量的写指令后会加入写屏障

    可见性: 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    有序性: 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

  2. 对 volatile 变量的读指令前会加入读屏障

    可见性: 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

    有序性:读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

volatile 性能

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

1.3、volatile与三特性

  • 可见性(能保证)

    volatile能保证被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新,所以volatile能保证可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  • 有序性(能保证)

    volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。所以能保证可见性

    有序性即程序执行的顺序按照代码的先后顺序执行。

  • 原子性(不能保证)

    保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。所以,volatile是不能保证原子性的。一般使用加锁synchronized来保证原子性

    原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

1.4、volatile使用场景

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
  2. 变量不需要与其他状态变量共同参与不变约束。
  3. 无锁应用CAS

2、无锁应用CAS

synchronized关键字保证同步的,这会导致有锁机制存在以下问题

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发 ⽣冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同⼀时间只能有⼀个线程在执⾏。

而另一个更加有效的锁就是乐观锁。

乐观锁⼜称为“⽆锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执⾏,⽆需加锁也⽆需等待。

⽽⼀旦多个线程发⽣冲突,乐观锁通常是使⽤⼀种称为CAS的技术来保证线程执⾏的安全性。

由于⽆锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天⽣免疫死锁。

2.1、什么是CAS

CAS的全称是⽐较并交换(Compare And Swap)在CAS中,有这样三个值内存位置(V)、预期原值(A)和新值(B),如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

简单例子

  1. 如果有⼀个多个线程共享的变量 i 原本等于5,我现在在线程A中,想把它设置为新的值6;
  2. 我们使⽤CAS来做这个事情;
  3. ⾸先我们⽤i去与5对⽐,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功, i 的值被设置成了6;
  4. 如果不等于5,说明 i 被其它线程改过了(⽐如现在 i 的值为2),那么我就什么也不做,此次CAS失败, i 的值仍然为2。

当多个线程同时使⽤CAS操作⼀个变量时,只有⼀个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然 也允许失败的线程放弃操作。

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
  • 打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

2.2、Java实现CAS的原理

CAS是⼀种原⼦操作。那么Java是怎样来使⽤CAS的呢?我们知道,在Java中,如果⼀个⽅法是native的,那Java就不负责具体实现它,⽽是交给底层的JVM使⽤c或者c++去实现。

在Java中,有⼀个 Unsafe 类,它在 sun.misc 包中。它⾥⾯是⼀些 native ⽅法, 其中就有⼏个关于CAS的:

 public native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
 public native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
 public native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

Unsafe中对CAS的实现是C++写的,它的具体实现和操作系统、CPU都有关系。

Linux的X86下主要是通过 cmpxchgl 这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使⽤ lock 指令加锁来完成。当然不同的操作系统和处理器的实现会有所不同

Unsafe类⾥⾯还有其它⽅法⽤于不同的⽤途。⽐如⽀持线程挂起和恢复的 park 和 unpark , LockSupport类底层就是调⽤了这两个⽅法。还有⽀持反射操作的 allocateInstance() ⽅法。

2.3、 CAS原子操作

上⾯介绍了Unsafe类的⼏个⽀持CAS的⽅法。那Java具体是如何使⽤这⼏个⽅法来实现原⼦操作的呢?

JDK提供了⼀些⽤于原⼦操作的类,在 java.util.concurrent.atomic 包下⾯

常见的有

  • 原⼦更新基本类型 AtomicBoolean、AtomicInteger、AtomicLong
  • 原⼦更新数组 AtomicIntegerArray 、AtomicLongArray、 AtomicReferenceArray
  • 原⼦更新引⽤ AtomicReference、AtomicMarkableReference、AtomicStampedReference
  • 原⼦更新字段(属性) AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

AtomicInteger的使用

AtomicInteger i = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

2.4、源码分析AtomicInteger

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    // 设置为使用Unsafe.compareAndSwapInt进行更新
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //成员变量的偏移量
    private static final long valueOffset;
    //变量
	private volatile int value;
    static {
        try {
            //获得成员变量的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }   
}

获取和设置volatile修饰的变量

这个很简单,就是从主存中读数据和写数据

public final int get() {
    return value;
}
public final void set(int newValue) {
    value = newValue;
}

CAS的核心compareAndSet

可以看到,调用了unsafe.compareAndSwapInt() 方法,并设置了偏移量,原值,和期待值

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

获取并自增getAndIncrement

AtomicInteger类下的getAndIncrement()

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

Unsafe类下的getAndAddInt

//var1 =  this(AtomicInteger) ,var2=valueOffset(偏移量),var4=1(期望原值加1)
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
//根据类对象,和偏移量获取值
public native int getIntVolatile(Object var1, long var2);

var5根据类对象,和偏移量从主存中获取值,所以本质上还是调用了compareAndSwapInt,设置了值,这里使用了自选的方式,当获取的值,累增之后,var5的值与原主存的值不一样,即主存中的值被修改了,那var5就会重新从主存中获取,不断尝试去⽤CAS更新 ,如果更新失败,就继续重试,直到成功为止

2.5、CAS实现原⼦操作的三⼤问题

  • ABA问题

    所谓ABA问题,就是⼀个值原来是A,变成了B,⼜变回了A。这个时候使⽤CAS是检查不出变化的,但实际上却被更新了两次。

    ABA问题的解决思路是在变量前⾯追加上版本号或者时间戳。从JDK 1.5开始,JDK的atomic包⾥提供了⼀个类 AtomicStampedReference 类来解决ABA问题。

  • 循环时间⻓开销⼤

    CAS多与⾃旋结合。如果⾃旋CAS⻓时间不成功,会占⽤⼤量的CPU资源。

  • 只能保证⼀个共享变量的原⼦操作

    有两种解决⽅案:

    1. 使⽤JDK 1.5开始就提供的 AtomicReference 类保证对象之间的原⼦性,把多个 变量放到⼀个对象⾥⾯进⾏CAS操作;
    2. 使⽤锁。锁内的临界区代码可以保证只有当前线程能操作。

ABA问题解决演示

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
    System.out.println("main start...");
    // 获取值 A
    String prev = ref.getReference();
    // 获取版本号
    int stamp = ref.getStamp();
    System.out.println("版本 "+stamp);
    // 如果中间有其它线程干扰,发生了 ABA 现象
    other();
    Thread.sleep(1000);
    // 尝试改为 C
    System.out.println("change A->C {}"+ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() throws InterruptedException {
    new Thread(() -> {
        System.out.println("change A->B {}"+ref.compareAndSet(ref.getReference(), "B",
                ref.getStamp(), ref.getStamp() + 1));
        System.out.println("更新版本为 {}"+ ref.getStamp());
    }, "t1").start();
    Thread.sleep(500);
    new Thread(() -> {
        System.out.println("change B->A {}"+ ref.compareAndSet(ref.getReference(), "A",
                ref.getStamp(), ref.getStamp() + 1));
        System.out.println("更新版本为 {}"+ref.getStamp());
    }, "t2").start();
}

结果:

main start...
版本 0
change A->B {}true
更新版本为 {}1
change B->A {}true
更新版本为 {}2
change A->C {}false

12

评论区