Java无锁系列——2、CAS 实现原理 Unsafe 类简单介绍

本文详细解析了Java中CAS的实现原理,包括Unsafe类的介绍,以及如何利用Unsafe进行基础类型、对象引用、数组和对象属性的CAS更新。重点探讨了Unsafe的native方法在内存操作中的作用,确保了无锁同步的安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

在上篇博客中,我简单介绍了无锁同步 CAS 如何使用以及部分它的特性。本篇我打算整理一下 CAS 的实现原理即 Unsafe 类相关的知识。


Unsafe

本篇博客分以下几个模块展开:

  1. Unsafe 类简单介绍
  2. CAS 更新基础类型原理
  3. CAS 更新对象引用原理
  4. CAS 更新数组类型原理
  5. CAS 更新对象属性原理
  6. Unsafe 类方法总结
  7. Unsafe 类示例

1、Unsafe 类简单介绍

Unsafe 类处于包 sun.misc 下,该包由 sun 公司内部实现,不属于 J2EE 开发规范。

java 代码中任何 CAS 操作最终都是通过调用 Unsafe 类中的 native 方式实现,也就是说:Unsafe 通过调用操作系统底层资源实现任务。

从名称就可以看出,Unsafe 类是不安全的。它可以像C语言指针那样直接操作内存。一般我们不建议直接使用 Unsafe 类处理任务。


2、CAS 更新基础类型原理

上篇博客我们提到 CAS 可以更新基础类型数据,这里我们就拿 AtomicInteger 类的源码进行分析:

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;

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

通过上面的源码,我们可以看出:AtomicInteger 类的 CAS 方法最终还是调用了 Unsafe 类的方法。Unsafe 类方法的源码如下所示:

public native long objectFieldOffset(Field var1);

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

首先通过 Reflection.getCallerClass() 可以表明,调用此方法必须包含以下权限:

  • bootstrap class loader 加载的类可以调用
  • extension class loader 加载的类可以调用

而我们用户编写的类都是通过 application class loader 加载的,也就是说用户编写的代码不能通过这个静态方法直接获取对象实例,并且 Unsafe 类本身也没有提供公开的构造方法。

这样做的原因也非常明显:防止用户直接使用 Unsafe 类。然而事实上,只要愿意的话,同样可以通过反射创建该对象实例,相应出现的风险也需要程序员自己承担。

下面我们再看 AtomicInteger 对象调用 Unsafe 类方法时所传递的参数:

  • this:对象本身
  • valueOffset:AtomicInteger 对象 value 属性偏移地址
  • expect:期望值
  • update:新值

也就是说,AtomicInteger 首先根据 objectFieldOffset() 方法确定 value 属性在对象上的偏移值。然后将对象本身,偏移值,期望值,新值作为参数传递过去。在 compareAndSwapInt() 方法中:首先根据对象确定内存,然后根据偏移值获取到要操作的内存地址,直接拿期望值和内存中的值做比较,如果相等的话就将新值写入内存。

从这里也就可以看出 Unsafe 类直接通过操作内存完成 CAS 操作,调用的方法本身又是 native 方法,因此操作本身就是原子的,也就是说不会出现线程安全问题。

有了上面的铺垫,我们再来看另一个 AtomicInteger 常用的方法源码:

AtomicInteger 源码:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

Unsafe 源码:

public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
   do {
       var5 = this.getIntVolatile(var1, var2);
   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

   return var5;
}

public native int getIntVolatile(Object var1, long var2);

通过源码我们可以很清晰的看出,AtomicInteger 类的自增操作实际上也是通过 while 循环配合 unsafe 类方法实现的,最终执行成功后将计算的结果从内存中直接读出并返回。


3、CAS 更新对象引用原理

CAS 更新对象引用的原理实际上和更新基础类型相似,下面我们直接看源码:

AtomicReference 源码:

private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicReference.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile V value;

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

Unsafe 源码:

public native long objectFieldOffset(Field var1);

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

从源码来看,CAS 更新基础数据和更新对象引用的原理基本是相同的,这里我不做过多赘述。


4、CAS 更新数组类型原理

上篇博客中我们提到 CAS 更新数组类型有三种,这里我主要拿 AtomicIntegerArray 的源码来做说明:

AtomicIntegerArray 源码:

private static final int base = unsafe.arrayBaseOffset(int[].class);

private static final int shift;

static {
    int scale = unsafe.arrayIndexScale(int[].class);
    if ((scale & (scale - 1)) != 0)
        throw new Error("data type scale not a power of two");
    shift = 31 - Integer.numberOfLeadingZeros(scale);
}

public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

private boolean compareAndSetRaw(long offset, int expect, int update) {
    return unsafe.compareAndSwapInt(array, offset, expect, update);
}

private long checkedByteOffset(int i) {
    if (i < 0 || i >= array.length)
        throw new IndexOutOfBoundsException("index " + i);

    return byteOffset(i);
}

private static long byteOffset(int i) {
    return ((long) i << shift) + base;
}

Unsafe 源码:

public native int arrayIndexScale(Class<?> var1);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

下面我直接给出 compareAndSwapInt() 方法中这四个参数分别代表的意义:

  • array:数组本身
  • offset:对应要操作的下标元素地址
  • expect:期望值
  • update:更新值

事实上 Unsafe 实现 CAS 的方式大同小异,都是通过对象和偏移量,确定要操作的内存地址,直接从内存层面比较期望值后判断是否执行更新操作。

这里我们主要分析一下如何获取数组元素的内存地址:

  1. 首先通过 Unsafe 类的 arrayBaseOffset() 方法确定数组首个元素地址
  2. 其次通过 Unsafe 类的 arrayIndexScale() 方法确定数组中每个元素所占的空间大小
  3. 判断数组中元素对象所占的大小是否 2 的幂次方(硬规定)
  4. 通过 31 - Integer.numberOfLeadingZeros(scale) 计算出每个元素转二进制后,需要移动的零的数量
  5. 通过 byteOffset() 方法计算出参数下标所对应的实际地址
每个数组的元素地址 = 数组首元素地址 + 下标 * 每个数组元素的内存大小

numberOfLeadingZeros() 方法返回前缀 0 的数量:
32 位操作系统下,4转二进制为 00000000 00000000 00000000 00000100,此时前缀0的个数为29

我们对上述等式进行变形:

下标 * 每个数组元素的内存大小 变形为二进制形式:

假设数组中每个元素大小为4位,转换为2进制后表示为 100,此时 numberOfLeadingZeros() 方法计算前缀0有29个,31 - 29 后计算出后面有2个零。

address  = base + index * size
下标0:address = base + 0 * 4    等同于  base + 0 << 2
下标1:address = base + 1 * 4    等同于  base + 1 << 2
下标2:address = base + 2 * 4    等同于  base + 2 << 2
...

有了偏移量,后面的操作就没有什么好说的了,比较交换。


5、CAS 更新对象属性原理

最后我们再来看看,CAS 更新对象属性的原理。我们直接看代码:

AtomicIntegerFieldUpdater 源码:

@CallerSensitive
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                          String fieldName) {
    return new AtomicIntegerFieldUpdaterImpl<U>
        (tclass, fieldName, Reflection.getCallerClass());
}

AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                              final String fieldName,
                              final Class<?> caller) {
    final Field field;
    final int modifiers;
    try {
        field = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Field>() {
                public Field run() throws NoSuchFieldException {
                    return tclass.getDeclaredField(fieldName);
                }
            });
        modifiers = field.getModifiers();
        sun.reflect.misc.ReflectUtil.ensureMemberAccess(
            caller, tclass, null, modifiers);
        ClassLoader cl = tclass.getClassLoader();
        ClassLoader ccl = caller.getClassLoader();
        if ((ccl != null) && (ccl != cl) &&
            ((cl == null) || !isAncestor(cl, ccl))) {
            sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
        }
    } catch (PrivilegedActionException pae) {
        throw new RuntimeException(pae.getException());
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }

    if (field.getType() != int.class)
        throw new IllegalArgumentException("Must be integer type");

    if (!Modifier.isVolatile(modifiers))
        throw new IllegalArgumentException("Must be volatile type");
        
    this.cclass = (Modifier.isProtected(modifiers) &&
                   tclass.isAssignableFrom(caller) &&
                   !isSamePackage(tclass, caller))
                  ? caller : tclass;
    this.tclass = tclass;
    this.offset = U.objectFieldOffset(field);
}

public final boolean compareAndSet(T obj, int expect, int update) {
    accessCheck(obj);
    return U.compareAndSwapInt(obj, offset, expect, update);
}
        
private final void accessCheck(T obj) {
    if (!cclass.isInstance(obj))
        throwAccessCheckException(obj);
}  

由此可见,CAS 修改对象属性也是通过计算偏移地址的方式来实现,这里我简单叙述一下整个构造方法的大致流程:

  1. 获取对象所输入的属性值
  2. 获取修饰符,判断属性访问权限是否包含
  3. 判断属性类型是否 Integer
  4. 判断属性是否 volatile 修饰
  5. 计算该属性在对象内存的偏移量

有了偏移量,后面的操作就很大众了,这里我不做赘述。


6、Unsafe 类方法总结

看到这里,我想大家关于 CAS 实现的原理已经有了大概的认识。除了在 CAS 中使用,Unsafe 类能做的还有很多。这里我列举出以下常用的 Unsafe 方法:

// get put 属性类(其他基础类型省略):

// 根据对象和偏移量,从内存直接读数据
public native int getInt(Object var1, long var2);
// 根据对象和偏移量,将新数据写入内存
public native void putInt(Object var1, long var2, int var4);
// 根据对象和偏移量,从内存读取对象
public native Object getObject(Object var1, long var2);
// 根据对象和偏移量,从新对象写入内存
public native void putObject(Object var1, long var2, Object var4);
// 根据偏移量直接获取值
public native int getInt(long var1);
// 根据偏移量直接写值
public native void putInt(long var1, int var3);
...

// 内存操作类

// 获取指定地址内存值
public native long getAddress(long address);
// 设置给定地址的内存值
public native void putAddress(long address, long x);
// 分配指定大小的内存
public native long allocateMemory(long bytes);
// 指定地址分配内存
public native long reallocateMemory(long address, long bytes);
// 释放参数地址申请的内存
public native void freeMemory(long address);
// 将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);

// 内存地址类

// 获取字段在对象上的偏移量
public native long objectFieldOffset(Field f);
// 获取静态字段在对象上的偏移量
public native long staticFieldOffset(Field f);
// 获取数组首个元素地址
public native int arrayBaseOffset(Class arrayClass);
// 获取数组每个元素的大小
public native int arrayIndexScale(Class arrayClass);

// CAS 相关

// 通过 CAS 设置对象
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
// 通过 CAS 设置Integer
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
// 通过 CAS 设置 Long
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

这里我暂时先列举出这么多,以后有用到其他方法时再加。


7、Unsafe 类示例

前文我们提到,可以通过 反射 的手段创建 Unsafe 对象,关于反射的知识我们后面再做整理。最后我们看一组具体示例:

public class UnsafeDemo {

    private int id;
    private String name;

    @Override
    public String toString() {
        return "UnsafeDemo{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Test
    public void test() throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println("通过反射创建的 Unsafe 类:" + unsafe);

        UnsafeDemo unsafeDemo = (UnsafeDemo) unsafe.allocateInstance(UnsafeDemo.class);
        Class unsafeDemoClass = unsafeDemo.getClass();

        Field id = unsafeDemoClass.getDeclaredField("id");
        Field name = unsafeDemoClass.getDeclaredField("name");

        unsafe.putInt(unsafeDemo, unsafe.objectFieldOffset(id), 1);
        unsafe.putObject(unsafeDemo, unsafe.objectFieldOffset(name), "李明");
        
        System.out.println(unsafeDemo.toString());
    }
}

执行结果

通过反射创建的 Unsafe 类:sun.misc.Unsafe@64a294a6
UnsafeDemo{id=1, name='李明'}

从输出结果可以看出,unsafe 类确实可以通过反射的方式创建,并且直接操作内存修改属性。


参考
https://blog.youkuaiyun.com/aguda_king/article/details/72355807
https://blog.youkuaiyun.com/javazejian/article/details/72772470
https://www.jianshu.com/p/2c1be41f6e59
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值