Unsafe类的原理

Unsafe类的介绍

sun.misc.Unsafe是基于操作系统的原子操作类。它是Java中对大多数锁机制实现的最基础类,JDK 1.8和之前JDK版本的中sun.misc.Unsafe类可提供的方法有较大的变化,JDK1.8及之后的Unsafe类提供的方法更多。Unsafe类提供了硬件级别的原子操作

获取Unsafe类对象实例

Unsafe类是一个单例,调用的方法为 getUnsafe(),在获取实例的函数中有一步判断,判断检查该CallerClass是不是由系统类加载器BootstrapClassLoader加载,我们知道bootstrapClass是最高层的加载器,底层由C++实现,没有父类加载器,所有由该系统类加载器加载的类调用getClassLoader()会返回null,所以要检查类是否为BootstrapClassLoader加载器加载只需要检查该方法是不是返回null。

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

上边获得 theUnsafe 对象是java内部使用的,因为 JDK源码中对这个类进行了严格限制,我们不能通过常规new的方式去获取该类的实例(因为采用单例实现,构造方法是私有的),也不能通过Unsafe.getUnsafe 获得Unsafe对象实例(因为我们自己的类不是由系统类加载器加载的);
那么我们通过什么方式获得该对象实例?这里就用到 强大的反射机制 自带暴力访问buff :
在Unsafe类中有一个私有成员变量theUnsafe,因此我们可以通过反射将private单例实例的accessible设置为true,然后通过Field的get方法获取,如下。

Field f = Unsafe.class.getDeclaredField("theUnsafe"); //获得名为 theUnsafe 的属性 即使为私有 同样获得
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null); //如果为静态属性 则get 参数为null

Unsafe类分配、释放内存

类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应: malloc(), realloc(), free() [直接开辟内存,释放内存,操作处理使用指针];

public native long allocateMemory(long l);
public native long reallocateMemory(long l, long l1);
public native void freeMemory(long l);

Unsafe类定位对象某字段在内存中的位置

  • 字段的定位
    ObjectFieldOffSet:定位对象字段在主存中的偏移位置
    staticFieldOffset:定位静态字段在主存中的偏移位置
  • 获取偏移位置处int、long类型的值
    getIntVolatile:该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。
    getLong:该方法获取对象中offset偏移地址对应的long型field的值。
  • 数组中元素定位
    arrayBaseOffset:该方法可以获取数组第一个元素的偏移地址。
    arrayIndexScale:该方法可以获取数组的转换因子,也就是数组中元素的增量地址(如int类型的增量是4个字节,long类型的增量是8个字节)。
public final class Unsafe {
    public static final int ARRAY_INT_BASE_OFFSET;
    public static final int ARRAY_INT_INDEX_SCALE;

    public native long staticFieldOffset(Field field);
    public native int getIntVolatile(Object obj, long l);
    public native long getLong(Object obj, long l);
    public native int arrayBaseOffset(Class class1);
    public native int arrayIndexScale(Class class1);

    static 
    {
        ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset([I);
        ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale([I);
    }
}

使用Unsafe对象获取对象中属性字段在内存中的偏移量

// 开始使用unsafe对象,分别找到UserPojo对象中child属性和name属性的内存地址偏移量
// 首先是UserPojo类中的child属性,在内存中设定的偏移位置
Field field = UserPojo.class.getDeclaredField("child");
// 这就是一旦这个类实例化后,该属性在内存中的偏移位置
long offset = unsafe.objectFieldOffset(field);
System.out.println("child offset = " + offset);

// 然后是UserPojo类中的name属性,在内存中设定的偏移位置
Field fieldName = UserPojo.class.getDeclaredField("name");
long nameOffset = unsafe.objectFieldOffset(fieldName);
System.out.println("name offset = " + nameOffset);

CAS操作-Compare And Swap

Unsafe中除了compareAndSwapObject 这个方法外,还有两个类似的方法:unsafe.compareAndSwapInt和unsafe.compareAndSwapLong。这些方法的作用就是对属性进行比较并替换(俗称的CAS过程——Compare And Swap)。当给定的对象中,指定属性的值符合预期,则将这个值替换成一个新的值并且返回true;否则就忽略这个替换操作并且返回false。
CAS过程是sun.misc.Unsafe类中除了获取内存偏移量以外,提供的最重要的功能了——因为Java中很多基于“无同步锁”方式的功能实现原理都是基于CAS过程。

/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

具体应用实例:

UserPojo user = new UserPojo();
user.setName("yinwenjie");
user.setSex(11);
user.setUserId("userid");

// 获得sex属性的内存地址偏移量 
Field field = UserPojo.class.getDeclaredField("sex");
long sexOffset = unsafe.objectFieldOffset(field);

/* 
 * 比较并修改值
 * 1、需要修改的对象
 * 2、要更改的属性的内存偏移量
 * 3、预期的值
 * 4、设置的新值
 * */
// 为什么是Object而不是int呢?因为sex属性的类型是Integer不是int嘛
if(unsafe.compareAndSwapObject(user, sexOffset, 11, 13)) {
    System.out.println("更改成功!");
} else {
    System.out.println("更改失败!");
}

首先创建一个UserPojo类的实例对象,这个实例对象有三个属性name、sex和userId。接着我们找到sex属性在主存中设定的偏移量sexOffset,并进行CAS操作。请注意compareAndSwapObject方法的四个值:第一个值表示要进行操作的对象user,第二个参数通过之前获取的主存偏移量sexOffset告诉方法将要比较的是user对象中的哪个属性,第三个参数为技术人员所预想的该属性的当前值,第四个参数为将要替换成的新值。
那么将方法套用到以上的compareAndSwapObject执行过程中:如果当前user对象中sex属性为11,则将这个sex属性的值替换为13,并返回true;否则不替换sex属性的值,并且返回false。

Unsafe.getAndAddInt(Object, long, int)和类似方法

类似的方法还有getAndAddLong(Object, long, long),它们的作用是利用Unsafe的原子操作性,向调用者返回某个属性当前的值,并且紧接着将这个属性增加一个新的值。在java.util.concurrent.atomic代码包中,有一个类AtomicInteger,这个类用于进行基于原子操作的线程安全的计数操作,且这个类在JDK1.8+的版本中进行了较大的修改。以下代码示例了该类的getAndIncrement()方法中的实现片段:

public class AtomicInteger extends Number implements java.io.Serializable {
    ……
    private volatile int value;
    ……
    private static final long valueOffset;
    ……
    // 获取到value属性的内存偏移量valueOffset
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    ……
    /**
     * 这是JDK1.8中的实现
     * Atomically increments by one the current value.
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

通过以上代码的演示可以看到,AtomicInteger类中定义了一个value属性,并通过unsafe.objectFieldOffset方法获取到了这个属性在主存中设定的偏移量valueOffset。接着就可以在getAndIncrement方法中直接使用unsafe.getAndAddInt的方式,通过偏移量valueOffset将value属性的值加“1”。但是该方法的实现在JDK1.8之前的版本中,实现代码却是这样的:

// 获取偏移量valueOffset的代码类似,这里就不再展示了
……
public final int getAndIncrement() {
    // 一直循环,直到
    for (;;) 
  {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
……
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

在以上代码中,getAndIncrement方法内部会不停的循环,直到unsafe.compareAndSwapInt方法执行成功。但多数情况下,循环只会执行一次,因为多线程强占同一对象属性的情况并不是随时都会出现。
如果存在多线程进行访问这段for循环的代码 如果保证其结果是准确的呢,比如 100个线程执行 atomicinteger 的自增操作;
下面用结合一个图来说明:

我们看到如果三个线程来访问此基于乐观锁的代码时,首先线程3 率先到达 if 的地方 进行判断,假设此时current的值为10,此时 其他的 线程1 线程2 都停留在get方法的地方 获取到了对象属性的值10,此时如果线程3 根据预期值进行判断的过程中发现 current的值 与 对象属性value的值是一样的 此时会进行 CAS 操作,将对象的 value设置为当前的 next 值 也就是 11; 然后线程3 执行结束 ,如果此时线程1 获得执行权限 继续往下执行 此时线程1 的 current值 还是为 10 next的值为11 来到if的地方进行判断 此时 因为对象的属性value的值是11 与预期值 current不等 所以不行CAS操作; 然后继续进行新一轮的for循环,此时,继续get取得 current的值 是 11 next的值为12 ,此时进行if 测试的预期值和对象的value值是一样的 然后执行更新操作;此时对象value的值被更新为12 线程1执行完毕,然后就剩下了线程2该线程的执行过程与线程1类似。
从上面的过程可以看出,三个线程可以完成三次更新值得操作,并且没有加入同步锁。
在JDK1.8中Unsafe还有一些其它实用的原子操作方法:

  • PutXXXXX(Object, long, short)
    类似的方法包括:putInt(Object, long, int)、putBoolean(Object, long, boolean)、putShort(Object, long, short)、putChar(Object, long, char)、putDouble(Object, long, double)等,这些都是针对指定对象中在偏移量上的属性值,进行直接设定。这些操作发生在CPU一级缓存(L1) 或者二级缓存(L2)中,但是这些方法并不保证工作在其它内核上的线程“立即看到”最新的属性值。
  • putXXXXXVolatile(Object, long, byte)
    类似的方法包括:putByteVolatile(Object, long, byte)、putShortVolatile(Object, long, short)、putFloatVolatile(Object, long, float)、putDoubleVolatile(Object, long, double)等等,这些方法的主要作用虽然也是直接针对偏移量改变指定对象中的属性值,但是这些方法保证工作在其它内核上的线程能“立即看到”最新的属性值——也就是说这些方法满足volatile语义。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值