首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发基础:了解无锁CAS就从源码分析

Java并发基础:了解无锁CAS就从源码分析

作者头像
良月柒
发布2019-03-19 16:07:23
5170
发布2019-03-19 16:07:23
举报

CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

CSA 原理

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。 在 java.util.concurrent 下面的源码中,Atomic, ReentrantLock 都使用了Unsafe类中的方法来保证并发的安全性。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。

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

源码分析

下面来看一下 java.util.concurrent.atomic.AtomicInteger.javagetAndIncrement()getAndDecrement()是如何利用CAS实现原子性操作的。

AtomicInteger 源码解析

valueOffset 字段表示"value" 内存位置,在compareAndSwap 方法 ,第二个参数会用到.

关于偏移量

Unsafe 调用C 语言可以通过偏移量对变量进行操作

实现逻辑封装在 Unsafe 中 getAndAddInt 方法,继续往下看,Unsafe 源码解析

Unsafe 源码解析

在JDK8中追踪可见sun.misc.Unsafe这个类是无法看见源码的,打开openjdk8源码看

文件:openjdk-8-src-b132-03_mar_2014.zip

目录:openjdk\jdk\src\share\classes\sun\misc\Unsafe.java

通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。

下面是sun.misc.Unsafe.java类源码

获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:

https://www.cnblogs.com/pkufork/p/java_unsafe.html

利用 Unsafe 类的 JNI compareAndSwapInt 方法实现,使用CAS实现一个原子操作更新,

compareAndSwapInt 四个参数

1、当前的实例 2、实例变量的内存地址偏移量 3、预期的旧值 4、要更新的值

unsafe.cpp 深层次解析

上面的分析看起来比较多,不过主流程并不复杂。如果不纠结于代码细节,还是比较容易看懂的。接下来,我会分析 Windows 平台下的 Atomic::cmpxchg 函数。继续往下看吧。

上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下:

到这里 CAS 的实现过程就讲了,CAS 的实现离不开处理器的支持。以上这么多代码,其实核心代码就是一条带 lock 前缀的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx

通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger getAndIncrement自增操作实现过程,是基于无锁实现的。

CAS的ABA问题及其解决方案

假设这样一种场景,当第一个线程执行CAS(V,E,U)操作。在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:

这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下原子类

AtomicStampedReference类

AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境

底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例

接着看 java.util.concurrent.atomic.AtomicStampedReference类的compareAndSet()方法的实现:

同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,

单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法

到这我们就很清晰AtomicStampedReference的内部实现思想了,

通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,

只有两者都符合预期才会调用UnsafecompareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。

期望 Pair<V> cmp(A) == 当前内存存偏移量位置 Pair(V),就更新值 Pair<V> val(B)成功返回true 否则 false

通过以上原子更新方法,可见 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile关键字对存储实际的引用变量和int的版本号的Pair实例进行更新。

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CSA 原理
  • 源码分析
    • AtomicInteger 源码解析
      • Unsafe 源码解析
        • unsafe.cpp 深层次解析
        • CAS的ABA问题及其解决方案
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档