(三)Java并发-Volatile和CAS

TOC
  1. 1. volatile
    1. 1.0.1. LOCK 指令
  • 2. CAS
    1. 2.1. 悲观锁和乐观锁
    2. 2.2. CAS 过程
    3. 2.3. ABA 问题
    4. 2.4. CAS 原理
  • volatile

    一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,两个含义:

    • 1.volatile 保证了共享变量的可见性;
    • 2.禁止进行指令重排序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadSafeInteger {

    private int value;

    public synchronized int get() {
    return value;
    }

    public synchronized void set(int value) {
    this.value = value;
    }
    }

    上面的同步和下面的 volatile 修饰变量是等价的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadSafeInteger {

    private volatile int value;

    public int get() {
    return value;
    }

    public void set(int value) {
    this.value = value;
    }
    }

    volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。volatile 关键字不会引起线程上下文切换和线程调度,使用 volatile 还能解决重排序问题。

    LOCK 指令

    用 volatile 关键字修饰的共享变量,编译成字节码后增有 Lock 前缀指令。
    Lock 前缀的指令在多核处理器下:

    • 将当前处理器的缓存航的数据写回到系统内存
    • 写回内存的操作会使其他 CPU 里缓存了该内存地址的数据无效。
    • 处理器使用嗅探技术保证内部缓存、系统缓存和其他处理器的数据在总线上保持一致。

    CAS

    volatile 修饰变量,对共享变量做读取或者赋值操作时,具有原子性;但一个变量赋值给另一个变量是非原子性的。因此 volatile 和 CAS 结合实现原子操作。

    悲观锁和乐观锁

    悲观锁(Pessimistic Lock),顾名思义,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持悲观态度。线程每次修改数据之前,都会认为别的线程会修改,所以拿数据之前会先上锁,别的线程要想使用这个数据必须等待线程释放掉锁。synchronized 就是悲观锁的一种,也被成为独占锁。
    乐观锁(Optimistic Lock),顾名思义,它认为数据一般情况下不会造成冲突,所以在数据提交更新的时候,会对数据是否冲突进行检测,如果发现冲突则失败并重试,直到成功为止,可以称为自旋。

    CAS 过程

    乐观锁用到的主要机制就是 CAS。CAS 即(Compare and swap),也就是比较并替换,CAS 有三个操作数分别为:内存值 V,旧的预期值 A,新的值 B。处理过程为:

    • 1、首先获取内存中的值 A
    • 2、A 经过自增或其他计算后变为 B
    • 3、对比当前内存中的 V 和 A 是否相同,相同则 B 替换 A
      AtomicLong 的自增就是使用这种方式实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public final long incrementAndGet() {
    for (;;) {
    long current = get();(1)
    long next = current + 1;(2)
    if (compareAndSet(current, next))(3)
    return next;
    }
    }

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

    假如当前值为 1,那么线程 A 和检查 B 同时执行到了(3)时候各自的 next 都是 2,current=1,假如线程 A 先执行了 3,那么这个是原子性操作,会把档期值更新为 2 并且返回 1,if 判断 true 所以 incrementAndGet 返回 2.这时候线程 B 执行 3,因为 current=1 而当前变量实际值为 2,所以 if 判断为 false,继续循环,如果没有其他线程去自增变量的话,这次线程 B 就会更新变量为 3 然后退出。
    这里使用了无限循环使用 CAS 进行轮询检查,虽然一定程度浪费了 CPU 资源,但是相比锁来说避免的线程上下文切换和调度。

    ABA 问题

    线程 1 获取当前内存值为:A,其他线程讲内存的值改为 B,后又改为 A。此时内存中的 A 已经不是线程 1 获取的 A,这个问题叫做 ABA 问题。

    • 1、ABA 问题无影响的可以不做处理
    • 2、有影响的可以加版本号,每次修改数据都修改版本号,对比值时也对比版本号即可。

    CAS 原理

    1
    lock cmpxchg

    CAS 主要是通过 lock cmpxchg 指令实现,单核 CPU 只需要 cmpxchg 指令,多核 CPU 使用 lock 前缀指令。

    学习资料:
    [1] BrianGoetz, 戈茨, 童云兰。 Java 并发编程实战[M]. 机械工业出版社, 2012.
    [2] 并发编程网 – ifeve.com – 高并发编程必备基础