前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么是 Java 中的 Unsafe 与 CAS ?

什么是 Java 中的 Unsafe 与 CAS ?

作者头像
lyb-geek
发布2019-05-07 14:52:52
9880
发布2019-05-07 14:52:52
举报
文章被收录于专栏:Linyb极客之路Linyb极客之路

Unsafe

简单讲一下这个类。Java 无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM 还是开了一个后门,JDK 中有一个类 Unsafe,它提供了硬件级别的原子操作。

这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。

从第一行的描述可以了解到 Unsafe 提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。不过 Java 本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。

举两个例子,比方说:

代码语言:javascript
复制
public native long staticFieldOffset(Field paramField);

这个方法可以用来获取给定的 paramField 的内存地址偏移量,这个值对于给定的 field 是唯一的且是固定不变的。再比如说:

代码语言:javascript
复制
public native int arrayBaseOffset(Class paramClass);
public native int arrayIndexScale(Class paramClass);

前一个方法是用来获取数组第一个元素的偏移地址,后一个方法是用来获取数组的转换因子即数组中元素的增量地址的。最后看三个方法:

代码语言:javascript
复制
public native long allocateMemory(long paramLong);
public native long reallocateMemory(long paramLong1, long paramLong2);
public native void freeMemory(long paramLong);

分别用来分配内存,扩充内存和释放内存的。

当然这需要有一定的 C/C++ 基础,对内存分配有一定的了解,这也是为什么我一直认为 C/C++ 开发者转行做 Java 会有优势的原因。

CAS

CAS,Compare and Swap 即比较并交换,设计并发算法时常用到的一种技术,java.util.concurrent 包全完建立在 CAS 之上,没有 CAS 也就没有此包,可见 CAS 的重要性。

当前的处理器基本都支持 CAS,只不过不同的厂家的实现不一样罢了。CAS 有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做并返回 false。

CAS 也是通过 Unsafe 实现的,看下 Unsafe 下的三个方法:

代码语言:javascript
复制
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

就拿中间这个比较并交换 Int 值为例好了,如果我们不用 CAS,那么代码大致是这样的:

代码语言:javascript
复制
public int i = 1;

public boolean compareAndSwapInt(int j)
{
    if (i == 1)
    {
        i = j;
        return true;
    }
    return false;
}

当然这段代码在并发下是肯定有问题的,有可能线程 1 运行到了第 5 行正准备运行第 7 行,线程 2 运行了,把 i 修改为 10,线程切换回去,线程1由于先前已经满足第 5 行的 if 了,所以导致两个线程同时修改了变量 i。

解决办法也很简单,给 compareAndSwapInt 方法加锁同步就行了,这样,compareAndSwapInt 方法就变成了一个原子操作。CAS 也是一样的道理,比较、交换也是一组原子操作,不会被外部打断,先根据 paramLong/paramLong1 获取到内存当中当前的内存值 V,在将内存值 V 和原值 A 作比较,要是相等就修改为要修改的值 B,由于 CAS 都是硬件级别的操作,因此效率会高一些。

由CAS分析AtomicInteger原理

java.util.concurrent.atomic 包下的原子操作类都是基于 CAS 实现的,下面拿 AtomicInteger 分析一下,首先是 AtomicInteger 类变量的定义:

代码语言:javascript
复制
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;

关于这段代码中出现的几个成员属性:

1、Unsafe是 CAS 的核心类,前面已经讲过了。

2、valueOffset 表示的是变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的。

3、value 是用 volatile 修饰的,这是非常关键的。

下面找一个方法 getAndIncrement 来研究一下 AtomicInteger 是如何实现的,比如我们常用的 addAndGet 方法:

代码语言:javascript
复制
public final int addAndGet(int delta) {
    for (;;) {
        int current = get();
        int next = current + delta;
        if (compareAndSet(current, next))
            return next;
    }
}
代码语言:javascript
复制
public final int get() {
         return value;
}

这段代码如何在不加锁的情况下通过 CAS 实现线程安全,我们不妨考虑一下方法的执行:

1、AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据 Java 内存模型,线程 1 和线程 2 各自持有一份 value 的副本,值为 3。

2、线程 1 运行到第三行获取到当前的 value 为 3,线程切换。

3、线程 2 开始运行,获取到 value 为 3,利用 CAS 对比内存中的值也为 3,比较成功,修改内存,此时内存中的 value 改变比方说是 4,线程切换。

4、线程 1 恢复运行,利用 CAS 比较发现自己的 value 为 3,内存中的 value 为 4,得到一个重要的结论 –> 此时 value 正在被另外一个线程修改,所以我不能去修改它。

5、线程 1 的 compareAndSet 失败,循环判断,因为 value 是 volatile 修饰的,所以它具备可见性的特性,线程 2 对于 value 的改变能被线程 1 看到,只要线程 1 发现当前获取的 value 是 4,内存中的 value 也是 4,说明线程 2 对于 value 的修改已经完毕并且线程 1 可以尝试去修改它。

6、最后说一点,比如说此时线程 3 也准备修改 value 了,没关系,因为比较-交换是一个原子操作不可被打断,线程 3 修改了 value,线程 1 进行 compareAndSet 的时候必然返回的 false,这样线程 1 会继续循环去获取最新的 value 并进行 compareAndSet,直至获取的 value 和内存中的 value 一致为止。

整个过程中,利用 CAS 机制保证了对于 value 的修改的线程安全性。

CAS的缺点

CAS 看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且 CAS 从语义上来说也不是完美的,存在这样一个逻辑漏洞:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了 B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个漏洞称为 CAS 操作的 ”ABA” 问题。java.util.concurrent 包为了解决这个问题,提供了一个带有标记的原子引用类 ”AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。不过目前来说这个类比较”鸡肋”,大部分情况下 ABA 问题并不会影响程序并发的正确性,如果需要解决 ABA 问题,使用传统的互斥同步可能回避原子类更加高效。

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

本文分享自 Linyb极客之路 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Unsafe
  • CAS
  • 由CAS分析AtomicInteger原理
  • CAS的缺点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档