首页 > 编程语言 >#yyds盘点# Java双刃剑之Unsafe类详解

#yyds盘点# Java双刃剑之Unsafe类详解

时间:2024-03-27 23:03:23浏览次数:23  
标签:yyds Java int Object Unsafe 屏障 内存 方法 public

for (int i = 0; i < 2; i++) {

unsafe.copyMemory(null,addr,null,addr3+size*i,4);

}

System.out.println(unsafe.getInt(addr));

System.out.println(unsafe.getLong(addr3));

}finally {

unsafe.freeMemory(addr);

unsafe.freeMemory(addr3);

}

}

先看结果输出:

addr: 2433733895744

addr3: 2433733894944

16843009

72340172838076673

分析一下运行结果,首先使用 allocateMemory 方法申请4字节长度的内存空间,在循环中调用 setMemory 方法向每个字节写入内容为 byte 类型的1,当使用Unsafe调用 getInt 方法时,因为一个 int 型变量占4个字节,会一次性读取4个字节,组成一个 int 的值,对应的十进制结果为16843009,可以通过图示理解这个过程:

在代码中调用 reallocateMemory 方法重新分配了一块8字节长度的内存空间,通过比较 addr和 addr3 可以看到和之前申请的内存地址是不同的。在代码中的第二个for循环里,调用 copyMemory 方法进行了两次内存的拷贝,每次拷贝内存地址 addr 开始的4个字节,分别拷贝到以 addr3和 addr3+4 开始的内存空间上:

拷贝完成后,使用 getLong 方法一次性读取8个字节,得到 long 类型的值为72340172838076673。

需要注意,通过这种方式分配的内存属于堆外内存,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用 freeMemory 方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在 try 中执行对内存的操作,最终在 finally 块中进行内存的释放。

2、内存屏障

在介绍内存屏障前,需要知道编译器和CPU会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致CPU的高速缓存和内存中数据的不一致,而内存屏障( Memory Barrier )就是通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。

在硬件层面上,内存屏障是CPU为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在java8中,引入了3个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由jvm来生成内存屏障指令,来实现内存屏障的功能。Unsafe中提供了下面三个内存屏障相关方法:

//禁止读操作重排序

public native void loadFence();

//禁止写操作重排序

public native void storeFence();

//禁止读、写操作重排序

public native void fullFence();

内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以 loadFence 方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。

看到这估计很多小伙伴们会想到 volatile 关键字了,如果在字段上添加了 volatile 关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改 flag 标志位,注意这里的 flag 是没有被 volatile 修饰的:

@Getter

class ChangeThread implements Runnable{

/**volatile**/ boolean flag=false;

@Override

public void run() {

try {

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、对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的 putInt 、 getInt 方法外,Unsafe提供了全部8种基础数据类型以及 Object 的 put 和 get 方法,并且所有的 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 屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。

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

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操作,像在前面介绍 synchronized 和 AQS 的文章中也多次提到了CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe类中,提供了 compareAndSwapObject 、 compareAndSwapInt 、 compareAndSwapLong 方法来实现的对 Object、 int 、 long 类型的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();

}

}

}

运行代码会依次输出:

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

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

6、线程调度

Unsafe类中提供了 park 、 unpark 、 monitorEnter 、 monitorExit 、 tryMonitorEnter 方法进行线程调度,在前面介绍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) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

img

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
img

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

[外链图片转存中…(img-tpWSVg5t-1711529753739)]

[外链图片转存中…(img-ZSeCfjO6-1711529753739)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

标签:yyds,Java,int,Object,Unsafe,屏障,内存,方法,public
From: https://blog.csdn.net/2401_83601703/article/details/137082748

相关文章

  • 2024年Java高分面试指南横空出世!1000道面试题+300W字解析
    42、java中有没有指针?43、java中是值传递引用传递?44、实例化数组后,能不能改变数组长度呢?45、假设数组内有5个元素,如果对数组进行反序,该如何做?46、形参与实参区别47、构造方法能不能显式调用?48、什么是方法重载?49、构造方法能不能重写?能不能重载?50、内部类......
  • #私藏项目实操分享# Java实现基于朴素贝叶斯的情感词分析
    另外,在贝叶斯公式的基础上进行变形,可以得到下面的公式:$$P(B_i|A)=\frac{P(B_i)P(A|B i)}{\sum {j=1}^nP(B_j)P(A|B_j)}$$其中 B1,B2,…,Bj 是一个完备事件组,上面的公式可以表示在事件A已经发生的条件下,寻找导致A发生的各种“原因”的 Bi 的概率。朴素贝叶斯......
  • 区块链编程七大语言,使用最多的竟是Java
    SQL——结构化查询语言(StructuredQueryLanguage)或“Sequel”,是IBM开发的一种编程语言,用于与存储、查询和处理数据的数据库进行沟通。如今SQL约拥有700万名开发者。MySQL、PostgreSQL、SQLServer、DB2、Oracle等主流数据库都使用SQL来开发应用程序。使用SQL的区块链项......
  • 学习java时候的笔记(四)
    数组什么是数组?数组指的是一种容器,可以用来储存同种数据类型的多个值一维数组一维数组的定义:格式1数据类型[]数组名例:int[]array格式2数据类型数组名[]例intarray[]一维数组的静态初始化初始化:就是在内存中,为数组容器开辟空间,并将数据存入容器中......
  • java基础 韩顺平老师的 面向对象(高级) 自己记的部分笔记
     373,类变量引出 代码就提到了问题分析里的3点packagecom.hspedu.static_;publicclassChildGame{publicstaticvoidmain(String[]args){//定义一个变量count,统计有多少小孩加入了游戏intcount=0;Childchild1=newChild(......
  • 【面试精讲】Java垃圾回收算法分析和代码示例
    【面试精讲】Java垃圾回收算法分析和代码示例目录一、引用计数(ReferenceCounting)算法二、可达性分析(ReachabilityAnalysis)算法三、标记-清除(Mark-Sweep)算法四、复制(Copying)算法五、标记-整理(Mark-Compact)算法六、分代收集(GenerationalCollection)算法七、死亡对象判......
  • Java学习路线
    Java学习路线可以分为几个阶段,每个阶段都有其特定的学习目标和内容。以下是一条详细的Java学习路线:###阶段1:Java入门-**目标**:培养兴趣、快速上手。-**前期准备**:准备好在线编程工具,如菜鸟工具,以及记笔记软件,如Typora。-**Java编程基础**(约45天): -Java特点、环......
  • 抽象类java
    packagedemo;abstractclassPerson{//私有数据成员privateStringname;publicPerson(){}publicPerson(Stringname){this.name=name;}//getter和setter方法publicStringgetName(){returnname;}......
  • JAVA面试大全之并发篇
    目录1、并发基础1.1、多线程的出现是要解决什么问题的?本质什么?1.2、Java是怎么解决并发问题的?1.3、线程安全有哪些实现思路?1.4、如何理解并发和并行的区别?1.5、线程有哪几种状态?分别说明从一种状态到另一种状态转变有哪些方式?1.6、通常线程有哪几种使用方式?1......
  • Java写一个计算机,要求实现加减乘除功能,并且能够循环接收新的数据,通过用户交互来实现
    importjava.util.Scanner;//写一个计算机,要求实现加减乘除功能,并且能够循环接收新的数据,通过用户交互来实现publicclassDemo08{publicstaticvoidmain(String[]args){inta=0;intb=0;chars......