2024年Java最全Java双刃剑之Unsafe类详解(1),docker面试题

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“subThread change flag to:” + flag);

flag = true;

}

}

在主线程的while循环中,加入内存屏障,测试是否能够感知到flag的修改变化:

public static void main(String[] args){

ChangeThread changeThread = new ChangeThread();

new Thread(changeThread).start();

while (true) {

boolean flag = changeThread.isFlag();

unsafe.loadFence(); //加入读内存屏障

if (flag){

System.out.println(“detected flag changed”);

break;

}

}

System.out.println(“main thread end”);

}

运行结果:

subThread change flag to:false

detected flag changed

main thread end

而如果删掉上面代码中的loadFence方法,那么主线程将无法感知到flag发生的变化,会一直在while中循环。可以用图来表示上面的过程:

了解java内存模型(JMM)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。

3、对象操作

a、对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的putIntgetInt方法外,Unsafe提供了全部8种基础数据类型以及Objectputget方法,并且所有的put方法都可以越过访问权限,直接修改内存中的数据。阅读openJDK源码中的注释发现,基础数据类型和Object的读写稍有不同,基础数据类型是直接操作的属性值(value),而Object的操作则是基于引用值(reference value)。下面是Object的读写方法:

//在对象的指定偏移地址获取一个对象引用

public native Object getObject(Object o, long offset);

//在对象指定偏移地址写入一个对象引用

public native void putObject(Object o, long offset, Object x);

除了对象属性的普通读写外,Unsafe还提供了volatile读写有序写入方法。volatile读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和Object类型,以int类型为例:

//在对象的指定偏移地址处读取一个int值,支持volatile load语义

public native int getIntVolatile(Object o, long offset);

//在对象指定偏移地址处写入一个int,支持volatile store语义

public native void putIntVolatile(Object o, long offset, int x);

相对于普通读写来说,volatile读写具有更高的成本,因为它需要保证可见性和有序性。在执行get操作时,会强制从主存中获取属性值,在使用put方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。

有序写入的方法有以下三个:

public native void putOrderedObject(Object o, long offset, Object x);

public native void putOrderedInt(Object o, long offset, int x);

public native void putOrderedLong(Object o, long offset, long x);

有序写入的成本相对volatile较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:

  • Load:将主内存中的数据拷贝到处理器的缓存中

  • Store:将处理器缓存的数据刷新到主内存中

顺序写入与volatile写入的差别在于,在顺序写时加入的内存屏障类型为StoreStore类型,而在volatile写入时加入的内存屏障是StoreLoad类型,如下图所示:

在有序写入方法中,使用的是StoreStore屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Store2以及后续的存储指令操作。而在volatile写入中,使用的是StoreLoad屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Load2及后续的装载指令,并且,StoreLoad屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。

综上所述,在上面的三类写入方法中,在写入效率方面,按照putputOrderputVolatile的顺序效率逐渐降低,

b、使用Unsafe的allocateInstance方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:

@Data

public class A {

private int b;

public A(){

this.b =1;

}

}

分别基于构造函数、反射以及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,说明通过allocateInstance方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了Class对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将A类的构造函数改为private类型,将无法通过构造函数和反射创建对象,但allocateInstance方法仍然有效。

4、数组操作

在Unsafe中,可以使用arrayBaseOffset方法可以获取数组中第一个元素的偏移地址,使用arrayIndexScale方法可以获取数组中元素间的偏移地址增量。使用下面的代码进行测试:

private void arrayTest() {

String[] array=new String[]{“str1str1str”,“str2”,“str3”};

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 : str1str1str

20 : str2

24 : str3

通过配合使用数组偏移首地址和各元素间偏移地址的增量,可以方便的定位到数组中的元素在内存中的位置,进而通过getObject方法直接获取任意位置的数组元素。需要说明的是,arrayIndexScale获取的并不是数组中元素占用的大小,而是地址的增量,按照openJDK中的注释,可以将它翻译为元素寻址的转换因子scale factor for addressing elements)。在上面的例子中,第一个字符串长度为11字节,但其地址增量仍然为4字节。

那么,基于这两个值是如何实现的寻址和数组元素的访问呢,这里需要借助一点在前面的文章中讲过的Java对象内存布局的知识,先把上面例子中的String数组对象的内存布局画出来,就很方便大家理解了:

在String数组对象中,对象头包含3部分,mark word标记字占用8字节,klass point类型指针占用4字节,数组对象特有的数组长度部分占用4字节,总共占用了16字节。第一个String的引用类型相对于对象的首地址的偏移量是就16,之后每个元素在这个基础上加4,正好对应了我们上面代码中的寻址过程,之后再使用前面说过的getObject方法,通过数组对象可以获得对象在堆中的首地址,再配合对象中变量的偏移量,就能获得每一个变量的引用。

5、CAS操作

juc包的并发工具类中大量地使用了CAS操作,像在前面介绍synchronizedAQS的文章中也多次提到了CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe类中,提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的CAS操作。以compareAndSwapInt方法为例:

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

参数中o为需要更新的对象,offset是对象o中整形字段的偏移量,如果这个字段的值与expected相同,则将字段的值设为x这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用compareAndSwapInt的例子:

private volatile int a;

public static void main(String[] args){

CasTest casTest=new CasTest();

new Thread(()->{

for (int i = 1; i < 5; i++) {

casTest.increment(i);

System.out.print(casTest.a+" ");

}

}).start();

new Thread(()->{

for (int i = 5 ; i <10 ; i++) {

casTest.increment(i);

System.out.print(casTest.a+" ");

}

}).start();

}

private void increment(int x){

while (true){

try {

long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField(“a”));

if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))

break;

} catch (NoSuchFieldException e) {

e.printStackTrace();

}

}

}

运行代码会依次输出:

1 2 3 4 5 6 7 8 9

在上面的例子中,使用两个线程去修改int型属性a的值,并且只有在a的值等于传入的参数x减一时,才会将a的值变为x,也就是实现对a的加一的操作。流程如下所示:

需要注意的是,在调用compareAndSwapInt方法后,会直接返回truefalse的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在AtomicInteger类的设计中,也是采用了将compareAndSwapInt的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。

6、线程调度

Unsafe类中提供了parkunparkmonitorEntermonitorExittryMonitorEnter方法进行线程调度,在前面介绍AQS的文章中我们提到过使用LockSupport挂起或唤醒指定线程,看一下LockSupport的源码,可以看到它也是调用的Unsafe类中的方法:

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);

}

LockSupport的park方法调用了Unsafe的park方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用unpark方法唤醒当前线程。下面的例子对Unsafe的这两个方法进行测试:

public static void main(String[] args) {

Thread mainThread = Thread.currentThread();

new Thread(()->{

try {

TimeUnit.SECONDS.sleep(5);

System.out.println(“subThread try to unpark mainThread”);

unsafe.unpark(mainThread);

} catch (InterruptedException e) {

e.printStackTrace();

}

}).start();

System.out.println(“park main mainThread”);

unsafe.park(false,0L);

System.out.println(“unpark mainThread success”);

}

程序输出为:

park main mainThread

subThread try to unpark mainThread

unpark mainThread success

程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用park方法阻塞自己,子线程在睡眠5秒后,调用unpark方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:

此外,Unsafe源码中monitor相关的三个方法已经被标记为deprecated,不建议被使用:

//获得对象锁

@Deprecated

public native void monitorEnter(Object var1);

//释放对象锁

@Deprecated

public native void monitorExit(Object var1);

//尝试获得对象锁

@Deprecated

public native boolean tryMonitorEnter(Object var1);

monitorEnter方法用于获得对象锁,monitorExit用于释放对象锁,如果对一个没有被monitorEnter加锁的对象执行此方法,会抛出IllegalMonitorStateException异常。tryMonitorEnter方法尝试获取对象锁,如果成功则返回true,反之返回false

7、Class操作

Unsafe对Class的相关操作主要包括类加载和静态变量的操作方法。

a、静态属性读取相关的方法:

//获取静态属性的偏移量

public native long staticFieldOffset(Field f);

//获取静态属性的对象指针

public native Object staticFieldBase(Field f);

//判断类是否需要实例化(用于获取类的静态属性前进行检测)

public native boolean shouldBeInitialized(Class<?> c);

创建一个包含静态属性的类,进行测试:

@Data

public class User {

public static String name=“Hydra”;

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);

}

运行结果:

false

Hydra

在Unsafe的对象操作中,我们学习了通过objectFieldOffset方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用staticFieldOffset方法。在上面的代码中,只有在获取Field对象的过程中依赖到了Class,而获取静态变量的属性时不再依赖于Class

在上面的代码中首先创建一个User对象,这是因为如果一个类没有被实例化,那么它的静态属性也不会被初始化,最后获取的字段属性将是null。所以在获取静态属性前,需要调用shouldBeInitialized方法,判断在获取前是否需要初始化这个类。如果删除创建User对象的语句,运行结果会变为:

true

null

b、使用defineClass方法允许程序在运行时动态地创建一个类,方法定义如下:

public native Class<?> defineClass(String name, byte[] b, int off, int len,

ClassLoader loader,ProtectionDomain protectionDomain);

在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(ClassLoader)和保护域(ProtectionDomain)来源于调用此方法的实例。下面的例子中实现了反编译生成后的class文件的功能:

private static void defineTest() {

String fileName=“F:\workspace\unsafe-test\target\classes\com\cn\model\User.class”;

File file = new File(fileName);

try(FileInputStream fis = new FileInputStream(file)) {

byte[] content=new byte[(int)file.length()];

fis.read(content);

Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);

Object o = clazz.newInstance();

Object age = clazz.getMethod(“getAge”).invoke(o, null);

System.out.println(age);

} catch (Exception e) {

e.printStackTrace();

}

}

在上面的代码中,首先读取了一个class文件并通过文件流将它转化为字节数组,之后使用defineClass方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过JVM的所有安全检查。

除了defineClass方法外,Unsafe还提供了一个defineAnonymousClass方法:

public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用ASM动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在jdk15发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用Unsafe的defineAnonymousClass方法。

最后

看完美团、字节、腾讯这三家的面试问题,是不是感觉问的特别多,可能咱们又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了1000道题目,多少还是有点用的呢,我看了下,上面这些问题大部分都能从我背的题里找到的,所以今天给大家分享一下互联网工程师必备的面试1000题

注意不论是我说的互联网面试1000题,还是后面提及的算法与数据结构、设计模式以及更多的Java学习笔记等,皆可分享给各位朋友

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

互联网工程师必备的面试1000题

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用ASM动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在jdk15发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用Unsafe的defineAnonymousClass方法。

最后

看完美团、字节、腾讯这三家的面试问题,是不是感觉问的特别多,可能咱们又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了1000道题目,多少还是有点用的呢,我看了下,上面这些问题大部分都能从我背的题里找到的,所以今天给大家分享一下互联网工程师必备的面试1000题

注意不论是我说的互联网面试1000题,还是后面提及的算法与数据结构、设计模式以及更多的Java学习笔记等,皆可分享给各位朋友

[外链图片转存中…(img-uqXaccii-1714857604156)]

互联网工程师必备的面试1000题

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题

[外链图片转存中…(img-RiZLSRAh-1714857604156)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值