【Java 并发】【七】【Unsafe】什么是Unsafe及其作用
1 前言
这节我们来看看JDK底层的unsafe,因为很多的操作都是依赖于unsafe提供的功能的。
2 unsafe是什么?
unsafe是JDK提供的一个工具类,里面的方法大多是native方法,unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。
(1)比如说通过unsafe可以让操作系统直接给你分配内存、释放内存。
(2)突破java语法本身的限制,直接从内存级别去操作堆里面的某个对象的数据;
(3)调用操作系统的CAS指令,实现CAS的功能
(4)操作系统层次将线程挂起和恢复
(5)提供操作系统级别的内存屏障(之前说过的Load屏障和Store屏障),读取数据强制走主存,修改数据直接刷新到主存
总之unsafe就相当于JDK给你提供的一个直接跟操作系统打交道的一个工具类,通过unsafe可做一些非常底层的指令和行为。
unsafe提供了很多操作系统级别的方法,在提供使用者便利的同时,也是隐藏着很多风险的。万一使用者分配大量的内存,没有及时回收,岂不是很容易造成内存溢出的风险?又或者分配了内存,但是忘记回收了,容易造成内存泄露。但是unsafe提供的这些操作系统级别的方法对于JDK底层的一些工具类、上层的一些框架来说在实现层方便了许多。比如著名的并发基础工具类AQS底层就是通过unsafe提供的CAS操作来进行加锁的,加锁失败的线程又是通过unsafe提供的park、unpark操作将线程挂起和唤醒的。还有一些非常著名的开源框架比如netty分配直接内存的方式底层也还是通过unsafe分配直接内存。
下面啊,我们分几类将一些unsafe提供的一些重要功能。
3 unsafe直接分配和释放内存
下面看一下unsafe提供的直接分配、释放内存,操作内存的一下方法:
// 分配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语法的限制,提供内存级别的直接获取数据,修改数据的方式;直接通过内存地址address,找到这块内存然后直接操作这个内存块的数据了。unsafe是通过调用操作系统提供的能力直接去申请和释放内存的。
4 unsafe提供的CAS操作
JUC提供的很多Atomic原子类、基于AQS实现的并发工具,底层都是通过CAS操作去实现的。下面我们就说说unsafe提供的cas操作:
假如目前有一个Test类是这样子的:
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
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提供了如下两个方法:
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其实是一样的,这里就不再赘述了。
5 unsafe将线程挂起和恢复
unsafe类提供类将一个线程挂起、讲一个挂起的线程唤醒的方法,分别是park和unpark,我们看如下的代码:
park方法:
//线程调用该方法,线程将一直阻塞直到被唤醒,或者超时, //或者中断条件出现。 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方法:
public native void unpark(Object thread);
直接将正在被挂起的thread线程唤醒,让它继续干活
这里我们再提一个类LockSupport,LockSupport是对unsafe中park和unpark功能封装的一个工具类,提供了阻塞和唤醒功能。
我们可以直接使用LockSupport的方法达到挂起和恢复线程的效果,LockSupport方法的源码如下:
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类的功能进行封装,然后我们就直接使用这些封装的工具类即可。
6 内存屏障
unsafe提供了几种内存屏障:
// 在该方法之前的所有读操作,一定在load屏障之前执行完成 public native void loadFence(); // 在该方法之前的所有写操作,一定在store屏障之前执行完成 public native void storeFence(); // 在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能 public native void fullFence();
7 unsafe类的cas是怎么保证原子性的?
这里我专门讲unsafe提供的cas功能,这个cas的功能是我们后面将Atomic原子类体系的基础。关于unsafe的cas功能底层是怎么保证原子性的?在操作系统层面是怎么实现的?由于的后面的并发知识非常多的使用到了unsafe的cas功能,就要把把CAS底层的原理弄懂。
上面我们所说的CAS操作的时候,是直接通过( 对象地址 + 对象内部属性偏移量offset )直接定位到要修改的变量在内存的位置,然后在内存级别的比较数据和修改数据。操作的时候直接根据 o对象地址 + offset偏移量地址,定位到demo属性在内存的位置,然后直接操作内存修改数据。
由于CPU是不会直接读写主存的,数据读取的时候还是先将数据读取到高速缓存,然后通过高速缓存传递给CPU.
写数据的时候也是先高速缓存,然后再将高速缓存的数据写入内存;于是可以得到下面的图形:
如果在多线程并发操作的时候会有什么问题?多个线程或者多个CPU同时读取demo属性的时候,可能会导致数据不一致我问题啊,比如我拿一个 x++ 的例子来说:
比如CPU0、CPU1都通过内存地址定位到 x 所在位置,然后同时读取 x = 0,然后同时执行x++操作,再刷新会主内存,这个时候就会导致 x 的值不是我们想要的。
在多个CPU都可以同时操作一个共享变量的时候,就会出现这个问题。CAS操作是可以保证原子性的,也就是同一个时间,同一个操作只允许一个CPU操作成功,它这个又是怎么保证的呢?
其实CAS底层的操作,还是会用到锁的,只不过这个锁是比较轻量级的,不会导致线程沉睡,下面我们来看看CAS加锁来保证原子性的原理,CAS底层硬件使用锁保证原子性。
(1)首先CPU0要执行CAS操作对变量 i 进行赋值,然后CPU0告诉总线说我要申请单独操作变量 i 的权限,帮我告诉一下CPU1等其它的CPU兄弟
(2)总线通知到了CPU1,CPU1告诉总线,好的,我不会操作数据,让CPU0大胆的去操作吧
(3)然后总线告诉CPU0,你可以独占变量i的操作了,其它的兄弟表示不会干扰你
(4)然后CPU0从自己的缓存读取 变量 i 的值;然后又根据 (o对象地址 + offset偏移量地址) 直接定位到变量 i 在内存的位置,直接读取变量i在内存的值
(5)接下来的操作就简单了,由于不会有人干扰,直接对比缓存的值和内存的值是否一致就可以了,如果一致,我直接修改,然后刷回主内存;如果不一致,说明我本地的数据不是最新的,需要重新申请CAS操作。
CAS在底层操作的原理,它底层还是通过加锁来保证原子性的,这个加锁是在硬件级别的,非常轻量级的。
8 小结
这节我们看了unsafe提供的几类操作系统级别的功能,比较重要的还是:内存级别操作数据,cas操作,线程挂起park和唤醒unpark,还针对cas的原子性做了详细的解释,有理解不对的地方欢迎指正哈。