当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!
一、写在开头
依稀记得多年以前的一场面试中,面试官从Java并发编程问到了锁,从锁问到了原子性,从原子性问到了Atomic类库(对着JUC包进行了刨根问底),从Atomic问到了CAS算法,紧接着又有追问到了底层的Unsafe类,当问到Unsafe类时,我就知道这场面试废了,这似乎把祖坟都能给问冒烟啊。
但时过境迁,现在再回想其那场面试,不再觉得面试官的追毛求疵,反而为那时候青涩菜鸡的自己感到羞愧,为什么这样说呢,实事求是的说Unsafe类虽然是比较底层,并且我们日常开发不可能用到的类,但是!翻开JUC包中的很多工具类,只要底层用到了CAS思想来提升并发性能的,几乎都脱离不了Unsafe类的运用,可惜那时候光知道被八股文了,没有做到细心总结与发现。
二、Unsafe的基本介绍
我们知道C语言可以通过指针去操作内存空间,Java不存在指针,为了提升Java运行效率、增强Java语言底层资源操作能力,便诞生了Unsafe类,Unsafe是位于sun.misc包下。正如它的名字一样,这种操作底层的方式是不安全的,在程序中过度和不合理的使用,会带来未知的风险,因此,Unsafe虽然,但要慎用哦!
2.1 如何创建一个unsafe实例
我们无法直接通过new的方式创建一个unsafe的实例,为什么呢?我们看它的这段源码便知:
public final class Unsafe {
// 单例对象
private static final Unsafe theUnsafe;
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在启动类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
从源码中我们发现Unsafe类被final修饰,所以无法被继承,同时它的无参构造方法被private修饰,也无法通过new去直接实例化,不过在Unsafe 类提供了一个静态方法getUnsafe,看上去貌似可以用它来获取 Unsafe 实例。但是!当我们直接去调用这个方法的时候,会报如下错误:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
这是因为在getUnsafe方法中,会对调用者的classLoader进行检查,判断当前类是否由Bootstrap classLoader加载,如果不是的话就会抛出一个SecurityException异常。
那我们如果想使用Unsafe类,到底怎样才能获取它的实例呢?
在这里提供给大家两种方式:
方式一
假若在A类中调用Unsafe实例,则可通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被启动类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
方式二
利用反射获得 Unsafe 类中已经实例化完成的单例对象:
public static Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
return unsafe;
}
2.2 Unsafe的使用
上面我们已经知道了如何获取一个unsafe实例了,那现在就开始写一个小demo来感受一下它的使用吧。
public class TestService {
//通过单例获取实例
public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
return unsafe;
}
//调用实例方法去赋值
public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
Persion persion = new Persion();
persion.setAge(10);
System.out.println("ofigin_age:" + persion.getAge());
long fieldOffset = unsafe.objectFieldOffset(Persion.class.getDeclaredField("age"));
System.out.println("offset:"+fieldOffset);
unsafe.putInt(persion,fieldOffset,20);
System.out.println("new_age:"+unsafe.getInt(persion,fieldOffset));
}
public static void main(String[] args) {
TestService testService = new TestService();
try {
testService.fieldTest(getUnsafe());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
class Persion{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
输出:
ofigin_age:10
offset:12
new_age:20
通过 Unsafe 类的objectFieldOffset方法获取到了对象中字段的偏移地址,这个偏移地址不是内存中的绝对地址而是一个相对地址,之后再通过这个偏移地址对int类型字段的属性值进行读写操作,通过结果也可以看到 Unsafe 的方法和类中的get方法获取到的值是相同的。
三、Unsafe类的8种应用
基于Unsafe所提供的API,我们大致可以将Unsafe根据应用场景分为如下的八类,上一个脑图。
3.1 内存操作
学习过C或者C++的同学对于内存操作应该很熟悉了,在Java里我们是无法直接对内存进行操作的,我们创建的对象几乎都在堆内内存中存放,它的内存分配与管理都是JVM去实现,同时,在Java中还存在一个JVM管控之外的内存区域叫做“堆外内存”,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法啦。
内存操作的常用方法:
/*包含堆外内存的分配、拷贝、释放、给定地址值操作*/
//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);
在这里我们不仅会想,为啥全是native方法呢?
- native方法通过JNI调用了其他语言,如果C++等提供的现车功能,可以让Java拿来即用;
- 需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用;
- 程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。
【经典应用】
在Netty、MINA等NIO框架中我们常常会应到缓冲池,而实现缓冲池的一个重要类就是DirectByteBuffer,它主要的作用对于堆外内存的创建、使用、销毁等工作。
通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存
从上图我们可以看到,在构建实例时,DirectByteBuffer内部通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。
3.2 内存屏障
为了充分利用缓存,提高程序的执行速度,编译器在底层执行的时候,会进行指令重排序的优化操作,但这种优化,在有些时候会带来 有序性 的问题。(在将volatile关键字的时候提到过了)
为了解决这一问题,Java中引入了内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令),通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。
在Unsafe类中提供了3个native方法来实现内存屏障:
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
【经典应用】
在之前的文章中,我们讲过Java8中引入的一个高性能的读写锁:StampedLock(锁王),在这个锁中同时支持悲观读与乐观读,悲观读就和ReentrantLock一致,乐观读中就使用到了unsafe的loadFence(),一起去看一下。
/**
* 使用乐观读锁访问共享资源
* 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候 可能其他写线程已经修改了数据,
* 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
*
* @return
*/
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 获取乐观读锁
double currentX = x, currentY = y; // 拷贝共享资源到本地方法栈中
if (!sl.validate(stamp)) { // //检查乐观读锁后是否有其他写锁发生,有则返回false
stamp = sl.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
在官网给出的乐观读的使用案例中,我们看到if中做了一个根绝印章校验写锁发生的操作,我们跟入这个校验源码中:
public boolean validate(long stamp) {
U.loadFence();//load内存屏障
return (stamp & SBITS) == (state & SBITS);
}
这一步的目的是防止锁状态校验运算发生重排序导致锁状态校验不准确的问题!
3.3 对象操作
其实在2.2 Unsafe的使用中,我们已经使用了Unsafe进行对象成员属性的内存偏移量获取,以及字段属性值的修改功能了,除了Int类型,Unsafe还支持对所有8种基本数据类型以及Object的内存数据修改,这里就不再赘述了。
需要额外强掉的一点,在Unsafe的源码中还提供了一种非常规的方式进行对象的实例化:
//绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
这种方法可以绕过构造方法和初始化代码块来创建对象,我们写一个小demo学习一下。
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
定义一个类A,我们分别采用无参构造器、newInstance()、Unsafe方法进行实例化。
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
输出结果为1,1,0。这说明调用unsafe的allocateInstance方法确实可以跳过构造器去实例化对象!
3.4 数组操作
在 Unsafe 中,可以使用arrayBaseOffset方法获取数组中第一个元素的偏移地址,使用arrayIndexScale方法可以获取数组中元素间的偏移地址增量,通过这两个方法可以定位数组中的每个元素在内存中的位置。
基于2.2 Unsafe使用的测试代码,我们增加如下的方法:
//获取数组元素在内存中的偏移地址,以及偏移量
private void arrayTest(Unsafe unsafe) {
String[] array=new String[]{"aaa","bb","cc"};
int baseOffset = unsafe.arrayBaseOffset(String[].class);
System.out.println("数组第一个元素的偏移地址:" + baseOffset);
int scale = unsafe.arrayIndexScale(String[].class);
System.out.println("元素偏移量" + scale);
for (int i = 0; i < array.length; i++) {
int offset=baseOffset+scale*i;
System.out.println(offset+" : "+unsafe.getObject(array,offset));
}
}
输出:
数组第一个元素的偏移地址:16
元素偏移量4
16 : aaa
20 : bb
24 : cc
3.5 CAS相关
终于,重点来了,我们写这篇文章的初衷是什么?是回想起曾经面时,面试官由原子类库(Atomic)问到了CAS算法,从而追问到了Unsafe类上,在JUC包中到处都可以看到CAS的身影,在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等等类中均有!
以AtomicInteger为例,在内部提供了一个方法为compareAndSet(int expect, int update) ,如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update),而它的底层调用则是unsafe的compareAndSwapInt()方法。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CAS思想的底层实现其实就是Unsafe类中的几个native本地方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
3.6 线程调度
Unsafe 类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度,在前面介绍 AQS 的文章中我们学过,在AQS中通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
LockSupport源码:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
3.7 Class操作
Unsafe 对Class的相关操作主要包括静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
//获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
【测试案例】
@Data
public class User {
public static String name="javabuild";
int age;
}
private void staticTest() throws Exception {
User user=new User();
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
//获取给定静态字段的内存地址偏移量
long fieldOffset = unsafe.staticFieldOffset(sexField);
//获取一个静态类中给定字段的对象指针
Object fieldBase = unsafe.staticFieldBase(sexField);
//根据某个字段对象指针和偏移量可以唯一定位这个字段。
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
此外,在Java8中引入的Lambda表达式的实现中也使用到了defineClass和defineAnonymousClass方法。
3.8 系统信息
Unsafe 中提供的addressSize和pageSize方法用于获取系统信息。
1) 调用addressSize方法会返回系统指针的大小,如果在 64 位系统下默认会返回 8,而 32 位系统则会返回 4。
2) 调用 pageSize 方法会返回内存页的大小,值为 2 的整数幂。
使用下面的代码可以直接进行打印:
private void systemTest() {
System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());
}
输出为:8,4096
四、总结
哎呀,妈呀,终于写完了,人要傻了,为了整理这篇文章看了大量的源码,人看的头大,跟俄罗斯套娃似的源码,严谨的串联在一起!Unsafe类在日常的面试中确实不经常被问到,大家稍微了解一下即可。
五、结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!的更多相关文章
- 手撕面试官系列(五):Tomcat+Mysql+设计模式面试专题
Tomcat (面试题+答案领取方式见侧边栏) Tomcat 的缺省端口是多少,怎么修改? tomcat 有哪几种 Connector 运行模式(优化)? Tomcat 有几种部署方式? tomcat ...
- 书评<<剑指offer 名企面试官精讲典型编程题>>
前前后后阅读了一周, 感慨很多, 面试考察的是一个人的综合能力, 这一点从面试官的角度去解读, 确实对面试的理解更立体. *) 具体考察的点1) 扎实的基础2) 高质量的代码3) 清晰的思路4) ...
- 走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串
原文:走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串 对大量数据进行查询时,可以应用到索引技术.索引是一种特殊类型的数据库对象,它保存着数据表中一列或者多列的排序结果,有 ...
- 以技术面试官的经验分享毕业生和初级程序员通过面试的技巧(Java后端方向)
本来想分享毕业生和初级程序员如何进大公司的经验,但后来一想,人各有志,有程序员或许想进成长型或创业型公司或其它类型的公司,所以就干脆来分享些提升技能和通过面试的技巧,技巧我讲,公司你选,两厢便利. 毕 ...
- 金三银四,如何征服面试官,拿到Offer
又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往年「冷」一些,形式更加严峻一些,大家多多少少可能都听到或看到一些信息,就是好多公司在优化裁 ...
- 当面试官说 “你还有什么问题想问的” ,你该如何回答?
阅读本文大概需要 4 分钟. 作者:黄小斜 来源:程序员江湖 程序员面试时经常会听到面试官说一些套话,比如"今天的面试就到这里了,回去等通知吧","你还有什么问题想问我的 ...
- 如何征服面试官,拿到Offer [转]
转自 https://my.oschina.net/cccyb/blog/3012768 又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往 ...
- 大厂面试官:Java工程师的“十项全能”
想要成为合格的Java程序员或工程师到底需要具备哪些专业技能,在面试之前到底需要准备哪些东西呢?面试时面试官想了解你的什么专业技能,以下都是一个合格Java软件工程师所要具备的. 一.专业技能 熟练的 ...
- 利用递归,反射,注解等,手写Spring Ioc和Di 底层(分分钟喷倒面试官)了解一下
再我们现在项目中Spring框架是目前各大公司必不可少的技术,而大家都知道去怎么使用Spring ,但是有很多人都不知道SpringIoc底层是如何工作的,而一个开发人员知道他的源码,底层工作原理,对 ...
- 《吊打面试官》系列-Redis基础
你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...
随机推荐
- Python 内置数据类型详解
内置数据类型 在编程中,数据类型是一个重要的概念. 变量可以存储不同类型的数据,不同类型可以执行不同的操作. Python默认内置了以下这些数据类型,分为以下几类: 文本类型:str 数值类型:int ...
- 标签栏切换效果 JS
标签栏切换效果 JS 要求:class为tab-box的元素用于实现标签栏的外边框,,分别实现标签栏的标签部分和内容部分. html <div class="tab-box" ...
- 当JDBC遇上返回Cursor
当 JDBC 遇上返回 Cursor 本文出处:https://www.modb.pro/db/400426 使用 jdbc 访问 PostgreSQL 或者 MogDB(openGauss)数据库里 ...
- 重新整理.net core 计1400篇[九] (.net core 中的依赖注入的服务注入)
前言 在该系列六中介绍了一个简单的依赖注入,该节介绍.net core 中的依赖注入的服务注入. ServiceDescriptor ServiceDescriptor 是服务描述的意思,这个是做什么 ...
- Python Flask获取iOS的UDID
测试iOS app时候,我们可以安装以下4种类型的包 : AdHoc -- 一般为正式环境验证AppStore -- 上传AppStore,只有 ...
- 接口API用例自动转locust测试用例
做接口测试是必要的,写完接口测试用例,再写locust压测脚本,其实差异不大: 写个简单的py,把接口测试脚本转为locust压测脚本,本例只是简单的示范: 原接口校验脚本: 1 # -*- codi ...
- 试用阿里云GPU服务器进行深度学习模型训练
试用阿里云GPU服务器进行深度学习模型训练 最近在用PyTorch时发现在本地训练模型速度一言难尽,然后发现阿里云可以白嫖gpu服务器,只要没有申请过PAI-DSW资源的新老用户都可以申请5000CU ...
- 【云原生】拿下 Gartner 容器产品第一,阿里云打赢云原生关键一战!
近日,Gartner 发布 2020 年公共云容器报告,据报告显示,阿里云和 AWS 拥有最丰富的产品布局,覆盖 9 项产品能力,并列排名第一. 据 Gartner 分析师评论,阿里云拥有丰富的容器产 ...
- 如何从 0 到 1 开发 PyFlink API 作业
简介: 以 Flink 1.12 为例,介绍如何使用 Python 语言,通过 PyFlink API 来开发 Flink 作业. Apache Flink 作为当前最流行的流批统一的计算引擎,在实时 ...
- 双11特刊|购物车实时显示到手价,看云原生内存数据库Tair如何提升用户体验?
阿里云自研内存数据库Tair诞生于2009年,是一种支持高并发低延迟访问的云原生内存数据库,完全兼容Redis,已历经多年双11大促考验,提供核心在线访问加速能力,显著提升系统吞吐量. 作为双11大 ...