正文前先来一波福利推荐:
福利一:
百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。
福利二:
毕业答辩以及工作上各种答辩,平时积累了不少精品PPT,现在共享给大家,大大小小加起来有几千套,总有适合你的一款,很多是网上是下载不到。
获取方式:
微信关注 精品3分钟 ,id为 jingpin3mins,关注后回复 百万年薪架构师 ,精品收藏PPT 获取云盘链接,谢谢大家支持!
-----------------------正文开始---------------------------
Unsafe类的介绍
Java中基于操作系统级别的原子操作类sun.misc.Unsafe,它是Java中对大多数锁机制实现的最基础类。请注意,JDK 1.8和之前JDK版本的中sun.misc.Unsafe类可提供的方法有较大的变化,本文基于JDK 1.8。sun.misc.Unsafe类提供的原子操作基于操作系统直接对CPU进行操作,而以下这些方法又是sun.misc.Unsafe类中经常被使用的:
java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供了以下功能:
1、获取Unsafe对象实例
Unsafe类是一个单例,调用的方法为 getUnsafe(),在获取实例的函数中有一步判断,判断检查该CallerClass是不是由系统类加载器BootstrapClassLoader
加载,我们知道bootstrapClass是最高层的加载器,底层由C++实现,没有父类加载器,所有由该系统类加载器加载的类调用getClassLoader()会返回null,所以要检查类是否为bootstrap加载器加载只需要检查该方法是不是返回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
2、通过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);
3、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;
字段的定位:
对象的字段在主存中的偏移位置:ObjectFieldOffSet staticFieldOffset
JAVA中对象的字段的定位可能通过staticFieldOffset方法实现,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的。
对象的整型或者双精度浮点型在主存中的偏移位置:getIntVolatile, getLong
getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。
getLong方法获取对象中offset偏移地址对应的long型field的值
对象的数组元素定位:arrayBaseOffset, arrayIndexScale
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
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);
}
}
4、CAS操作-Compare And Swap
Unsafe中除了objectFieldOffset(Field) 这个方法外,还有一个类似的方法staticFieldOffset(Field)。这两个方法用于返回类定义中某个属性在主存中设定的偏移量。请看以下代码:
// 注意本代码中的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);
测试结果为 打印对应各个对象属性在主存中的偏移位置;具体结果不贴出来了;
Unsafe中除了compareAndSwapObject 这个方法外,还有两个类似的方法:unsafe.compareAndSwapInt和unsafe.compareAndSwapLong。这些方法的作用就是对属性进行比较并替换(俗称的CAS过程——Compare And Swap)。当给定的对象中,指定属性的值符合预期,则将这个值替换成一个新的值并且返回true;否则就忽略这个替换操作并且返回false。
请注意CAS过程是sun.misc.Unsafe类中除了获取内存偏移量以外,提供的最重要的功能了——因为Java中很多基于“无同步锁”方式的功能实现原理都是基于CAS过程。
方法举例:CompareAndSwapInt()
/**
* 比较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);
}
这里采用的For循环进行原子atomicinteger操作;后边篇幅会写java的自旋锁以及自旋锁的一种具体实现方式“乐观锁”,还会分析“乐观锁”适合使用的场景。
在以上代码中,getAndIncrement方法内部会不停的循环,直到unsafe.compareAndSwapInt方法执行成功。但多数情况下,循环只会执行一次,因为多线程强占同一对象属性的情况并不是随时都会出现。
如果存在多线程进行访问这段for循环的代码 如果保证其结果是准确的呢,比如 100个线程执行 atomicinteger 的自增操作;
下面用结合一个图来说明:
我们看到如果三个线程来访问此基于乐观锁的代码时,首先线程3 率先到达 if 的地方 进行判断此时current的值为10,此时 其他的 线程2 线程3 都停留在get方法的地方 获取到了对象属性的值10
此时如果线程3 根据预期值进行判断的过程中发现 current的值 与 对象属性value的值是一样的 此时会进行 CAS 操作,将对象的 value设置当前的 next 值 也就是 11; 然后线程3 执行结束 如果此时
线程2 获得执行权限 继续往下执行 此时线程1 的 current值 还是为 10 next的值为11 来到if的地方进行判断 此时 因为对象的属性value的值是11 与预期值 current不等 所以不行CAS操作; 然后继续进行新一轮的
for循环 此时 继续get取得 current的值 是 11 next的值我 12 此时进行if 测试的预期值和对象的value值是一样的 然后执行更新操作;此时对象value的值被更新为12 线程2执行完毕,然后就剩下了线程 1 该线程的执行与线程2
的执行时一样的 需要进行一次新的for循环 才可以实现更新值得操作;
从上面的过程可以看出,三个线程可以完成三次更新值得操作,并且没有加入同步锁。
在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语义(后续文章会详细介绍volatile的详细工作原理)
总结:
Unsafe类的总结已经写完 ,基于该类使用的类有很多,除了原子数据 AtomicXXX, 还有LockSupport类 以及在 线程池 ThreadPool 类也是用了该类, 后边具体写这两个类。