Java 多线程与并发——CAS 操作和 AQS 框架

多线程与并发 专栏收录该内容
8 篇文章 0 订阅

1.CAS操作

像 synchronized 属于悲观锁,CAS(Compare and Swap,比较并交换)则属于乐观锁,是一种高效实现线程安全性的方法,支持原子更新操作,适用于计数器等场景。CAS 操作失败时由开发者决定是继续尝试,还是执行别的操作,因此支持失败的线程不会被阻塞挂起。

使用 volatile 关键词实现了实例变量在多个线程之间的可见性,但 volatile 关键词最致命的缺点是不支持原子性。所以多线程环境下,可以使用 synchronized 修饰,value++; 的操作变成了原子性操作,保证了线程安全:

public class VolatileVisibility {
    private static volatile int value = 0;
    public synchronized static void increase() {
        value++;
    }
}

由于 synchronized 会创建一个内存屏障,保证所有结果都刷新到主存中去,保证了操作的内存可见性,所以 volatile 也就可以省略了:

public class VolatileVisibility {
    private static int value = 0;
    public synchronized static void increase() {
        value++;
    }
}

该方法是可行的,但是有没有办法进一步提升性能呢?

应该尽量避免加 synchronized 这样的悲观锁。synchronized 会让没有得到锁资源的线程进入 BLOCKED 状态,而后在争夺到锁资源后恢复为 RUNNABLE 状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。尽管 JDK1.6 为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

此时可以使用 AtomicInteger 替代 synchronized 来满足这样的要求:

public class VolatileVisibility {
    private static AtomicInteger value = new AtomicInteger(0);
    public static void increase() {
        value.getAndIncrement(); // value++;
    }
}

AtomicInteger 是 Java 中的原子操作类,所谓原子操作类,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。例如 AtomicBoolean、AtomicInteger、AtomicLong,它们分别用于 Boolean、Integer、Long 类型的原子性操作。

使用 AtomicInteger 之后,同样可以保证线程安全。并且在某些情况下,代码的性能会比 synchronized 更好。

而 Atomic 操作类的底层,正是利用了 CAS 机制。

1.CAS原理

CAS 机制使用了 3 个基本操作数:内存地址 V,预期原值 A 和新值 B。

更新一个变量的时候,只有当变量的预期原值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B,否则 CPU 不做任何操作。

这样说或许有些抽象,我们来看一个例子:

  1. 在内存地址 V 当中,存储着值为 10 的变量;
  2. 此时线程 1 想要把变量的值增加 1,对线程 1 来说,旧的预期值 A=10,要修改的新值 B=11;
  3. 在线程 1 要提交更新之前,另一个线程 2 抢先一步,把内存地址 V 中的变量值率先更新成了 11;
  4. 线程 1 开始提交更新,首先进行 A 和地址 V 的实际值比较(Compare),发现 A 不等于 V 的实际值,提交失败;
  5. 线程 1 重新获取内存地址 V 的当前值,并重新计算想要修改的新值,此时对线程 1 来说,A=11,B=12,这个重新尝试的过程被称为自旋;
  6. 这一次没有其他线程改变地址 V 的值,线程 1 进行 Compare,发现 A 和地址 V 的实际值是相等的;
  7. 线程 1 进行 SWAP,把地址 V 的值替换为 B,也就是 12。

从思想上来说,synchronized 属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS 属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

那么在 Java 中都有哪些地方用到了 CAS 机制呢?

有许多地方用到,包括上面说的 Atomic 操作类以及 Lock 系列类的底层实现。甚至在 JDK1.6 以上版本,synchronized 转变为重量级锁之前,也会采用 CAS 机制。

2.AtomicInteger源码分析

AtomicInteger 是通过 sun.misc.Unsafe 这个硬件级别的原子操作类来实现 CAS 操作的。下面看下 AtomicInteger.java 的部分实现:

package java.util.concurrent.atomic;
import sun.misc.Unsafe;

public class AtomicInteger extends Number implements java.io.Serializable {
    // 取得Unsafe对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value; // 初始Integer大小
    // 省略了部分代码...

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    // 无参数构造函数, 默认初始大小为0
    public AtomicInteger() {
    }
    // 返回旧值, 并设置新值为newValue
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    // 获取当前值, 并设置新值+1, 同value++
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    // 省略了部分代码...
}

看下 getAndSet() 中 unsafe.getAndSetInt() 方法的实现:

public final class Unsafe {
    public final int getAndSetInt(Object o, long offset, int delta) {
        int v;
        do {
            // 获得当前主内存中的值
            v = this.getIntVolatile(o, offset); 
        // CAS 操作更新值, 如果失败就一直重试
        } while(!this.compareAndSwapInt(o, offset, v, delta));
        return v;
    }
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    // 省略了部分代码...
}

compareAndSwapInt() 方法便是 Integer 类型的 CAS 操作,这个方法是 native 的,属于 JVM 源码层面的实现。

3.CAS总结

CAS 多数情况下对开发者来说是透明的:

  • JUC 的 atomic 包(java.util.concurrent.atomic)提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选;
  • Unsafe 类虽然提供了 CAS 操作,但因能够操作任意内存地址读写而有隐患;
  • Java 9 及以上,可以使用 Variable Handle API 来替代 Unsafe 进行各种粒度的原子或者有序性的操作。

CAS 缺点:

  • 若更新一个变量,一直更新不成功而进行循环,则 CPU 开销很大;
  • 能保证代码块的原子性,只能保证一个共享变量的原子操作;
  • ABA 问题,这是 CAS 机制最大的问题所在。

什么是 ABA 问题?怎么解决?

如果地址 V 初次读取的值是 a,并且在准备赋值的时候检查到它的值仍然为 a,我们就可以说它的值没有被其他线程改变过了吗?是不能的,有可能中间被改成了 b,又改回了 a。而 CAS 操作就会认为从来没有被改过。这个问题就是 CAS 的 ABA 问题。

JUC 为了解决 ABA 问题,提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过变量值的版本来保证 CAS 的正确性。

在使用 CAS 前要考虑好 ABA 问题是否影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的 synchronized 可能会比原子类更高效。

2. AQS框架

AQS 全称 AbstractQueuedSynchronizers 抽象队列同步器,在 Java 并发包中很多锁都是通过 AQS 来实现加锁和释放锁的过程的,AQS 就是并发包基础。例如 ReentrantLock、ReentrantReadWriteLock 底层都是通过 AQS 来实现的。

AQS 1

AQS 内部其实就包含了三个组件:

  • state 资源状态;
  • exclusiveOwnerThread 持有资源的线程;
  • CLH 同步等待队列。

AQS 2

这张图描述了 ReentrantLock 和 AQS 的关系,ReentrantLock 其内部包含一个 AQS 对象(内部类),AQS 就是 ReentrantLock 可以获取和释放锁实现的核心部件。

ReentrantLock 加锁和释放锁底层原理实现:

ReentrantLock lock = new ReentrantLock();
try {
    lock.lock(); // 加锁
    // 业务逻辑代码
} finally {
    lock.unlock(); // 释放锁
}

这段代码加锁和释放锁到底会发生什么故事?

很简单在 AQS 内部有一个核心变量 (volatile)state 变量其代表了加锁的状态,初始值为 0。另外一个重要的关键 OwnerThread 持有锁的线程,默认值为 null,在回顾下这张图:

AQS 2

接着线程 1 过来通过 lock.lock() 方式获取锁,获取锁的过程就是通过 CAS 操作 volatile 变量 state 将其值从 0 变为 1。如果之前没有人获取锁,那么 state 的值肯定为 0,此时线程 1 加锁成功将 state = 1。

线程 1 加锁成功后还有一步重要的操作,就是将 OwnerThread 设置成为自己。如下图是线程 1 加锁的过程:

AQS 3

其实到这可以看出,AQS 其实就是并发包下面的一个核心组件,其内部维持 state 变量、线程变量等核心的东西,来实现加锁和释放锁的过程。

细心的同学可以注意到,不管是 ReentrantLock 还是 ReentrantReadWriteLock 都是 Reentrant 开头的,Reentrant 是可重入的意思,也就说其是一个可重入锁。

可重入锁就是你可以对一个 ReentrantLock 进行多次的 lock() 和 unlock() 操作,也就是可以对一个锁加多次,叫做可重入锁。来一段代码直观感受下:

ReentrantLock lock = new ReentrantLock();
try {
    lock.lock(); // 加锁1
    // 业务逻辑代码
    lock.lock() // 加锁2
    // 业务逻辑代码
    lock.lock() // 加锁3
} finally {
    lock.unlock(); // 释放锁3
    lock.unlock(); // 释放锁2
    lock.unlock(); // 释放锁1
}

注意:释放锁是由内到外依次释放的,不可缺少。

问题又来了,那么 ReentrantLock 内部又是如何来实现可重入的呢?

还是 AQS 这个核心组件帮我们实现的,重入就是判断当前锁是不是自己加上的,如果是就代表自己可以再次上锁,每重入一次就是将 state值加 1。就是这么简单啦!

说完了可重入我们再来看看锁的互斥又是如何实现的呢?

此时线程 2 也跑过来想加锁,CAS 操作尝试将 state 从 0 变成 1, 此时发现 state 已经不是0了,说明此锁已经被别人拿到了。接着线程 2 会判断一下这个锁是不是线程 2 加上的,发现 OwnerThread=线程1,明显不是自己加上的 ,加锁失败。来张图记录下线程 2 的加锁失败的过程:

AQS 4

线程 2 的加锁失败后,AQS 会将 线程 2 放入 CLH 等待队列中。

AQS 5

此时线程 1 业务执行完了,开始释放锁:

  • 将 state 值改为 0;
  • 将 OwnerThread 设为 null;
  • 通知线程 2 锁已经释放,该线程 2 获取锁了;

线程 2 立马开始尝试获取锁,CAS 尝试将 state 值设为 1,如果成功将 OwnerThread 设为自己 线程2。此时线程 2 便成功获取到了锁。

AQS 6

到这就借着 ReentrantLock 的加锁和释放锁的过程给大家讲解了一下 AQS 工作的原理。

用一句话总结下就是:AQS 就是 Java 并发包下的一个基础组件,用来实现各种锁和同步组件的,其核心为 Volatile state 变量、OwnerThread 加锁线程、CLH 同步等待队列等并发核心组件。

  • 12
    点赞
  • 0
    评论
  • 12
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值