JUC的基石:CAS + volatile 实现线程通信

CAS 自旋 volatile 变量,是一种很经典的用法,在 java.util.concurrent 包中随处可见,具体原因将在这篇文章中去进行介绍。

1、volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

存在问题volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

2、CAS

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。

这一系列的操作是原子的,CAS 通过调用 JNI (Java Native Interface)调用实现的。JNI允许 Java调用其他语言,而 CAS 就是借助 C 语言来调用 CPU 底层指令实现的。UnSafe 是 CAS 的核心类,它提供了硬件级别的原子操作。

3、CAS + volatile 

volatile 保证共享变量的可见性,CAS 保证更新操作的原子性,把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。如果仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  • 首先,声明共享变量为 volatile;

  • 然后,使用 CAS 的原子条件更新来实现线程之间的同步;

  • 同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

为什么无锁效率高?

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻

线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大

但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

4、源码分析

以AtomicInteger 类的 getAndIncrement()方法源码为例

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long valueOffset; //内存偏移量
    private static final Unsafe unsafe = Unsafe.getUnsafe(); //给Unsafe类的初始化,方便方法中调用。
    
	static {
        try {
            //给valueOffset初始化
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); 
        } catch (Exception ex) { throw new Error(ex); }
    }

    // 普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
    private volatile int value;
    
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

valueOffset 是什么? 

valueOffset 所代表的是AtomicInteger对象的value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存当前值。也就是说,valueOffset 是当前AtomicInteger对象初始化时的原始值的内存地址。例如:AtomicInteger atomicInteger = new AtomicInteger(5); 这个5就是原始值,即valueOffset 是5的内存地址。

 CAS + 自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 由于 value 声明为 volatile,所以以这种方式读取
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    // 在对象指定偏移地址处 volatile 读取一个int
    public native int getIntVolatile(Object var1, long var2);

结论:AtomicInteger 通过 CAS + 自旋 + volatile  value 保证自增方法成功执行。

5、总结

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的,理解这种用法对于源码的阅读会有很好的帮助。

参考文章:Java CAS(compare and swap)自旋操作(JUC基石--CAS+volatile实现线程通信) - 忙碌了一整天的L师傅 - 博客园 (cnblogs.com)

(2条消息) 多线程教程(二十四)CAS+volatile_今天成为大神了吗的博客-CSDN博客_cas,volatile在cpp文件的实现

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值