Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

栏目: IT技术 · 发布时间: 4年前

内容简介:作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:《Tomcat内核设计剖析》、《图解数据结构与算法》、《人工智能原理科普》。推荐作者的

关于Unsafe

Java从一开始就被定为一个安全的编程语言,它屏蔽了指针和内存的管理,从而减少犯错的风险。但 Java 仍然为我们留下了一个后门,通过这个后门能够进行一些低级别、不安全的操作,比如内存的申请/释放/访问等操作、底层硬件的原子操作、内存屏障、对象的操作等等。这个后门就是Unsafe类,该类位于sun.misc包下。在新版本的JDK中sum.misc包下的Unsafe类会间接调用jdk.internal.misc包下的Unsafe类,也就是说sun.misc.Unsafe的实现将全部委托给jdk.internal.misc.Unsafe。

如下图中,Java语言层能够通过Unsafe通道来执行底层的操作,这些操作可能涉及到JVM层的堆和方法区,当然也可能涉及到操作系统层,涉及到的更深一层则是硬件层。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
Unsafe类

主要类

Unsafe提供了很多与底层相关的操作,但对于并发和线程来说,我们主要关注与CAS和线程调度相关的方法。其中CAS包括compareAndSwapInt、compareAndSwapLong和compareAndSwapObject三个方法,而线程调度包括park和unpark两个方法。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
Unsafe并发相关

关于Unsafe的实例化

实际上JDK开发人员做了一些措施来避免Unsafe的滥用,它的设计是为了给JDK内部类库自身使用,所以它在实例化时增加了使用安全校验,必须是受信任代码才能对其进行实例化。该校验主要通过类加载器来判断,后面会讲到详细的判断逻辑。Unsafe提供了getUnsafe方法来获取该对象,所以我们第一种实例化方式就是直接调用该方法,但是这种方式对于我们Java语言层面开发者来说是行不通的,因为没办法通过安全校验,会抛出SecurityException异常。而第二种实例化方式则是通过反射机制来绕过安全检查,我们直接去修改Unsafe类中theUnsafe字段的访问权限使其能被访问,然后获取该字段的值便成功得到Unsafe对象。

所以关于Unsafe实例化工作给一个总结:JDK源码中涉及到Unsafe时实例化都可以直接通过getUnsafe方法,而如果我们在Java语言层面要实例化Unsafe时则需要通过反射的方式。也就是说,本书中所有模拟JDK并发类时都会使用反射方式去获取Unsafe对象。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
两种实例化方式

Unsafe例子

既然我们已经知道如何去实例化Unsafe对象了,那么接下去就编写一个小例子来理解Unsafe类。这个例子是使用Unsafe对象进行硬件级别的CAS操作,也就是修改UnsafeTest对象的flag字段。其中offset表示flag字段的地址偏移,由Unsafe的objectFieldOffset方法可以获得,这个地址偏移在调用compareAndSwapInt方法时将会作为其中一个参数,除了这个参数外还需要预期值和更新值。最终的输出结果如下:

flag字段的地址偏移为:12
CAS操作后flagֵ的值为:101
Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

Unsafe源码

我们只关注CAS、线程调度和构造函数相关的几个方法。如下代码所示,其中registerNatives方法用于注册本地方法。然后可以看到构造函数是private的,所以不能通过构造函数来实例化Unsafe对象。而getUnsafe方法则是我们可以用来获取Unsafe对象的方法,不过虽然它是public的,但它对调用者进行安全检查,它会判断调用者是不是由bootstrap类加载器加载,不是的话会抛出SecurityException异常。其它剩下的就是CAS和线程调度相关的方法,它们都是本地方法,后面我们逐个分析。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
Unsafe源码

compareAndSwapInt方法

前面说到compareAndSwapInt是一个本地方法,该方法对应的本地实现处于/openjdk/hotspot/src/share/vm/prims/unsafe.cpp中。对应的代码如下,具体的三步逻辑为:首先获取对象的oop,oop是JVM层一个对象的表示;然后获取该oop对象中对应偏移的地址,也就是Java层对象中某个字段的地址;最后通过Atomic::cmpxchg对指定地址去执行CPU级别的CAS操作。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

我们知道不同类型的CPU的指令集是不同的,此外不同的操作系统汇编语言也可能不同,那么就导致不同类型的CPU和不同操作系统都需要编写不一样的汇编语言。这里我们以x86架构的CPU为例,当处于 linux 系统下汇编则如下面所示,我们主关注其中的 cmpxchgl 指令,它便是CPU级别的CAS操作指令。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
linux x86

类似的,x86架构CPU在windows系统下的汇编如下所示,其中 cmpxchg 指令即是CPU级别的CAS操作指令。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
windows x86

compareAndSwapLong方法

compareAndSwapLong对应的本地实现也是在unsafe.cpp中,由于int是4字节的而long为8字节,所以底层的实现与compareAndSwapInt有些差别。具体代码如下,前面两行仍然通过偏移量来获取对象中某个字段的地址,接下去根据VM_Version::supports_cx8()会分两种情况处理。第一种是CPU指令支持8字节的CAS,那么则直接通过Atomic::cmpxchg来执行CAS操作。第二种是CPU不支持8字节的CAS,此时需要MutexLockerEx锁的协助才能完成CAS,步骤是先加锁,再通过Atomic::load获取对应地址的值,如果值与期望值不相等则直接返回false表示失败,否则继续调用Atomic::store来修改内存值,最终会自动释放锁。

在CPU支持8字节的CAS情况下,不同CPU类型和不同操作系统同样需要编写不同的汇编语言。以x86为例,linux下的汇编如下,此时会用到cmpxchgq指令来实现硬件级别的CAS操作。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
linux x86

而对于windows系统,汇编代码则如下,主要使用了cmpxchg8b指令来实现CAS操作。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
windows x86

compareAndSwapObject方法

compareAndSwapObject对应的本地实现如下,先分别获取三个对象的oop,对应为更新后的对象x、期望的对象e和待更新的对象p。然后获取待更新对象p的地址,接着调用oopDesc::atomic_compare_exchange_oop来对JVM层面的对象进行CAS操作,如果返回值不等于期望对象e则表示更新失败,直接返回false。最后是设置内存屏障,保证执行的顺序性和对其它线程的可见性。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

再详细看 oopDesc::atomic_compare_exchange_oop 核心方法,根据UseCompressedOops分两种情况处理,该函数表示JVM中对象的指针是否使用了压缩指针,压缩指针的目的是为了节约内存。对于压缩指针的情况,将值都转换成narrowOop类型,该类型其实是无符号整型,然后再调用了Atomic::cmpxchg进行CPU级别的CAS操作,最后再转成未压缩指针。而对于非压缩指针的情况,则调用 Atomic::cmpxchg_ptr 进行CPU级别的CAS操作,此时的指针大小为64位,可转成long型再执行CPU级别的64位的CAS操作。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

park方法

park方法的本地实现如下,其中我们只关注方框内的一行,这是实现park的核心方法,其它代码我们直接忽略掉。每个thread都有一个parker对应,由它来实现park操作。此外,由于不同的操作系统实现不一样,所以需要分多个系统各自实现,下面分别看linux和windows的实现。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

先看linux的实现,核心实际上就是使用了pthread库,通过它提供的互斥锁和条件等待等函数来实现park功能。 _counter 变量用于表示信号量,当可通过时为1不可通过时为0。如果为1就可以直接返回,因为已经有许可了,也就是先调用过unpark了。接着对时间time进行转换,然后尝试获取互斥锁,只有成功获取互斥锁才能往下执行。当time为0时调用不带超时的 pthread_cond_wait 进入阻塞而等待唤醒信号,否则调用带超时的 pthread_cond_timedwait 进入阻塞而等待唤醒信号。最终释放互斥锁。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
linux

对于windows的实现,则是直接通过windows提供的WaitForSingleObject函数进行park操作。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
windows

unpark方法

unpark实现如下,前面的逻辑都是为了获取线程对应的Parker指针,并非重点,其中我们只关注方框的那行代码。同样的,我们来看linux和windows对应的实现。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作

linux的unpark实现逻辑是:先通过pthread_mutex_lock获取互斥锁,然后将 _counter 的值修改为1,即表示许可为1。最后通过 pthread_cond_signal 去唤醒前面park中进入阻塞的线程。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
linux

windows的unpark则是简单调用SetEvent函数来设置许可信号,park操作中的WaitForSingleObject函数得到信号后则能往下执行。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作
windows

关于VarHandle

有时候在新版本的JDK中会看到用VarHandle替代了Unsafe的一部分功能,实际上它们实现的本质都类似,但按官方的说法是VarHandle更安全更易用更高性能,并且官方推荐不要使用Unsafe。不管如何,只要我们掌握了实现的原理,那么接口如何变化都能轻松应对。

总结

本文主要讲解了Unsafe这个低级别且不安全的类,通过该类能进行一些底层的操作。对于我们并发方面来说,它主要提供了五个相关的方法,其中三个用于CAS操作而另外两个用于线程调度操作。我们还介绍了在Java语言层面如何来实例化Unsafe对象,并提供了一个例子帮助大家理解。最后分别深入分析了Unsafe类提供的五个方法,从Java层到JVM层的实现代码,还包括不同的CPU架构和不同的操作系统的实现。

作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:《Tomcat内核设计剖析》、《图解数据结构与算法》、《人工智能原理科普》。

推荐作者的 一个 Java并发原理专栏,有需要的朋友可看看,已经有180+人参与学习。

Unsafe穿透Java层到JVM层,提供CPU级别和操作系统级别的操作


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Natural Language Processing with Python

Natural Language Processing with Python

Steven Bird、Ewan Klein、Edward Loper / O'Reilly Media / 2009-7-10 / USD 44.99

This book offers a highly accessible introduction to Natural Language Processing, the field that underpins a variety of language technologies, ranging from predictive text and email filtering to autom......一起来看看 《Natural Language Processing with Python》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具