14.JDK底层Unsafe类是个啥东西?

2023-10-16 10:06:35 浏览数 (2)

老王:小陈啊,从今天开始我们就要进入 《结丹篇》 了,在这一篇章里面,要注意听讲啊,对后面的每一个阶段的理解来说都至关重要的......

小陈:好的,老王,前面的《筑基》、《练气》两篇我已经重复看了好几遍了,早就期待进入下一个境界了......

老王:好,既然你这么有信心,那我们就正式进入主题了。

老王:首先啊,这一篇我们需要从JDK底层的unsafe开始讲起,后面的篇章很多都是依赖于unsafe提供的操作的。

JDK底层的unsafe是什么?

小陈:哦,unsafe?unsafe是个啥东西啊? 我还没有接触过

老王:说起unsafe啊,是JDK提供的一个工具类,unsafe里面的方法大多是native方法,你可以理解为unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。

(1)比如说通过unsafe可以让操作系统直接给你分配内存、释放内存

(2)突破java语法本身的限制直接从内存级别去操作堆里面的某个对象的数据

(3)调用操作系统的CAS指令,实现CAS的功能

(4)操作系统层次将线程挂起和恢复

(5)提供操作系统级别的内存屏障(之前说过的Load屏障和Store屏障),读取数据强制走主存,修改数据直接刷新到主存

总之unsafe就相当于JDK给你提供的一个直接跟操作系统打交道的一个工具类,通过unsafe可做一些非常底层的指令和行为

小陈:额,竟然可以直接通过unsafe分配内存,那岂不是不需要通过堆内存也可以直接分配内存了吗?这样岂不是很危险,万一使用者分配大量的内存,没有及时回收,岂不是很容易造成内存溢出的风险?又或者分配了内存,但是忘记回收了,容易造成内存泄露啊

老王:是啊,unsafe提供了很多操作系统级别的方法,在提供使用者便利的同时,也是隐藏着很多风险的。

但是unsafe提供的这些操作系统级别的方法对于JDK底层的一些工具类、上层的一些框架来说在实现层方便了许多。

比如著名的并发基础工具类AQS底层就是通过unsafe提供的CAS操作来进行加锁的,加锁失败的线程又是通过unsafe提供的park、unpark操作将线程挂起和唤醒的,还有一些非常著名的开源框架比如netty分配直接内存的方式底层也还是通过unsafe分配直接内存。

老王:所以啊,去了解一下unsafe底层的一些操作还是很有必要的,对于后面我们要学习的很多线程安全的类,比如Atomic系列的类基于AQS一些列的同步工具还是很有必要的,因为这些底层都是通过unsafe提供的操作去实现的。

小陈:哦哦,原来unsafe类这么重要啊......

老王:下面啊,我就分几类将一些unsafe提供的一些重要功能

unsafe直接分配和释放内存

代码语言:javascript复制
// 分配bytes大小的堆外内存
public native long allocateMemory(long bytes);
// 还可以执行从address处开始分配,分配bytes大小的堆外内存
public native long reallocateMemory(long address, long bytes);
// 释放allocateMemory和reallocateMemory申请的内存块
public native void freeMemory(long address);
// 将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
// 相当于直接让你在内存级别直接给这个对象的变量赋值了
public native void setMemory(Object o, long offset, long bytes, byte value);
 
// 设置给定内存地址的long值
// 相当于直接在内存级别给address后面的8个字节赋值
public native void putLong(long address, long x);
// 获取指定内存地址的long值
// 相当于获取address后面的8个字节的值,然后转化为十进制的long值给你
public native long getLong(long address);
// 设置或获取指定内存的byte值
// 相当于获取address后面一个字节的数据,转化成十进制返回给你
public native byte  getByte(long address);
// 直接在内存级别给adderess地址后面的1个字节设置
public native void  putByte(long address, byte x);

老王:这里提供一些操作系统级别的直接申请内存、释放内存的方式。

同时不受java语法的限制,提供内存级别的直接获取数据,修改数据的方式;unsafe操作内存这块内存我们目前只需要了解即可,对我们后续并发的学习影响不大。

老王:这里说可以直接分配内存,释放内存,直接从内存级别修改数据的操作理解了没?

小陈:这里说的通过操作系统级别的分配内存,释放内存,其实就是调用操作系统底层的api去申请和释放内存吧。

老王:是的,unsafe确实是通过调用操作提供的能力直接去申请和释放内存的。

小陈:上面说的不受java语法限制,直接修改内存数据是咋回事?

老王:比如说某个对象的private int value属性,这里的value属性是private的,如果这个对象没有提供对应的访问方法,在对象外部是访问不到这个value属性的对不对?

小陈:嗯嗯,是的,正常来说只有在对象的内部可以访问这个private属性,外部是访问不到的。

老王:然而这个对象的数据本质上还是保存在内存里面的,而unsafe提供了直接对某个内存地址读取、修改的操作,这样就可以突破java语法的限制了

小陈:哦哦,原来是这样啊,相当于就是直接通过内存地址address找到这块内存然后直接操作这个内存块的数据了。

老王:嗯嗯,没错;就是直接操作内存块的数据,接下来我们再讲讲unsafe提供的cas操作。

unsafe提供的CAS操作

老王:这块内容比较重要,JUC提供的很多Atomic原子类基于AQS实现的并发工具,底层都是通过CAS操作去实现的。下面我们就说说unsafe提供的cas操作:

老王:假如目前有一个Test类是这样子的:

代码语言:javascript复制
public class Test {
    private DemoClass demo;
    private int intValue;
    private long longValue;    
}

有一个Test类的对象 Test o = new Test();

这个时候想要突破java语法的限制,直接修改对象o的private修饰的demo属性。可以通过CAS操作直接去修改对象o里面的demo属性,使用unsafe提供的下面方法:

(1)o就是你要操作的对象

(2)offset就是demo属性在对象o内部的位置,或者偏移量

(3)expected就是demo期待的值

(4)x就是你希望设置的新值,只有demo的值 == expect的时候,才能将demo的值设置成x

代码语言:javascript复制
public final native boolean compareAndSwapObject(Object o,
                                                 long offset,
                                                 Object expected,
                                                 Object x);                                                                                                  

执行CAS操作大致是这样的,根据 对象o的地址,demo属性相对于o的偏移量offset,直接计算得到demo所在内存的位置,然后直接将demo的值从内存取出进行CAS(比较替换操作):

同理对于,执行CAS操作替换Test类对象o内部的int值和long值,unsafe提供了如下两个方法:

代码语言:javascript复制
public final native boolean compareAndSwapInt(Object o,
                                              long offset,
                                              int expected,
                                              int x);
 
public final native boolean compareAndSwapLong(Object o,
                                               long offset,
                                               long expected,
                                               long x);

底层执行CAS替换的原理跟上面画图将的demo其实是一样的,这里就不再赘述了。

unsafe将线程挂起和恢复

unsafe类提供类将一个线程挂起、讲一个挂起的线程唤醒的方法,分别是park和unpark,我们看如下的代码:

park方法
代码语言:javascript复制
//线程调用该方法,线程将一直阻塞直到被唤醒,或者超时,或者中断条件出现。  
public native void park(boolean isAbsolute, long time);  

(1) isAbsolute是否是绝对时间,当isAbsolute == true ,后面time的时间单位是ms;当为false的时候,后面time参数的时间单位是ns

(2)time > 0时候,表示大概要将线程挂起time的时间,过了时间后自动将线程唤醒。当time = 0的时候,表示一直将线程挂起,直到有人调用unpark方法将线程唤醒。

unpark方法
代码语言:javascript复制
public native void unpark(Object thread); 

直接将正在被挂起的thread线程唤醒,让它继续干活

LockSupport

LockSupport是对unsafe中park和unpark功能封装的一个工具类,提供了阻塞和唤醒功能

我们可以直接使用LockSupport的方法达到挂起和恢复线程的效果,LockSupport方法的源码如下:

代码语言:javascript复制
public class LockSupport {
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        // 这里直接调用unsafe的park方法将线程挂起
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    public static void unpark(Thread thread) {
        if (thread != null)
            // 直接调用unsafe的unpark方法将线程唤醒
            UNSAFE.unpark(thread);
    }
}

老王:由于我们自己编写的java程序不能直接使用unsafe工具类,所以啊JDK还是有一些工具类对unsafe类的功能进行封装,然后我们就直接使用这些封装的工具类即可。

内存屏障

unsafe提供了几种内存屏障:

代码语言:javascript复制
// 在该方法之前的所有读操作,一定在load屏障之前执行完成
public native void loadFence();
// 在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();
// 在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能
public native void fullFence();

老王:小陈啊,关于上述讲解的unsafe提供的几类操作系统级别的功能,理解清楚了吗?

小陈:恩恩,大致上清楚了,我看比较重要的还是:内存级别操作数据,cas操作,线程挂起park和唤醒unpark。

老王:那我们本章就先到这里了,我们下一章节再聊。

小陈:我们下一章见。

目录

JAVA并发专题 《筑基篇》

1.什么是CPU多级缓存模型?

2.什么是JAVA内存模型?

3.线程安全之可见性、有序性、原子性是什么?

4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?

JAVA并发专题《练气篇》

5.volatile怎么保证可见性?

6.什么是内存屏障?具有什么作用?

7.volatile怎么通过内存屏障保证可见性和有序性?

8.volatile为啥不能保证原子性?

9.synchronized是个啥东西?应该怎么使用?

10.synchronized底层之monitor、对象头、Mark Word?

11.synchronized底层是怎么通过monitor进行加锁的?

12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

13.synchronized怎么保证可见性、有序性、原子性?

JAVA并发专题《结丹篇》

  1. JDK底层Unsafe类是个啥东西?

15.unsafe类的CAS是怎么保证原子性的?

16.Atomic原子类体系讲解

17.AtomicInteger、AtomicBoolean的底层原理

18.AtomicReference、AtomicStampReference底层原理

19.Atomic中的LongAdder底层原理之分段锁机制

20.Atmoic系列Strimped64分段锁底层实现源码剖析

JAVA并发专题《金丹篇》

21.AQS是个啥?为啥说它是JAVA并发工具基础框架?

22.基于AQS的互斥锁底层源码深度剖析

23.基于AQS的共享锁底层源码深度剖析

24.ReentrantLock是怎么基于AQS实现独占锁的?

25.ReentrantLock的Condition机制底层源码剖析

26.CountDownLatch 门栓底层源码和实现机制深度剖析

27.CyclicBarrier 栅栏底层源码和实现机制深度剖析

28.Semaphore 信号量底层源码和实现机深度剖析

29.ReentrantReadWriteLock 读写锁怎么表示?

  1. ReentrantReadWriteLock 读写锁底层源码和机制深度剖析

JAVA并发专题《元神篇》并发数据结构篇

31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?

32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?

33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?

34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?

35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?

36.DelayQueue 底层源码剖析,延时队列怎么实现?

37.SynchronousQueue底层原理解析

JAVA并发专题《飞升篇》线程池底层深度剖析

  1. 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?

39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?

40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?

  1. ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
  2. ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
  3. ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
  4. ThreadPoolExecutor shutdown、shutdownNow内部核心流程
  5. 再回头看看为啥不推荐Executors提供几种线程池?
  6. ThreadPoolExecutor线程池篇总结

0 人点赞