Java魔法类 Unsafe
- 文章导读:(约12015字,阅读时间大约1小时)
- 1. Unsafe介绍
- 2. Unsafe创建
- 3. Unsafe功能
- 3.1内存操作
- 3.2 内存屏障
- 3.3 对象操作
- 3.4 数组操作
- 3.5 CAS操作
- 3.6 线程调度
- 3.7 Class操作
- 3.8 系统信息
- 4. 总结
JUC源码中的并发工具类出现过很多次 Unsafe类,它
的功能以及使用场景这篇进行介绍。
文章导读:(约12015字,阅读时间大约1小时)
1. Unsafe介绍
Unsafe是位于sun.misc包下的类,提供一些更底层的,访问系统内存资源,和管理系统内存资源的方法,但是因为会访问系统的内存资源 变成和C语言一样的指针,指针的使用是有风险的,所以Unsafe也是有类似的风险,所以在使用的时候需要注意,过度或者不正确的使用可能导致程序出错。
但是,Unsafe类也使得Java增强了底层操作系统资源的能力。
同时,Unsafe提供的功能的实现是依赖于本地方法(Native Method)的,本地方法就是Java中使用其他语言写的方法,本地方法用native修饰,java只声明方法,具体实现由本地方法实现。
使用本地方法的原因:
- 需要使用到Java没有的特性,就得使用本地方法 来用别的语言来实现,比如Java没有什么底层操作系统的能力,所以要想在跨平台的同时还可以由底层控制的能力,就要使用本地方法。
- 其他语言已经实现的功能,可以Java调用使用
- Java在速度上比一些更底层的语言慢,如果程序对时间要求高或者对性能要求高,那么就需要使用更底层的语言。
JUC包的很多并发工具类在实现并发功能的时候,都调用了本地方法,用来提高Java的运行上限,同时为了能更底层的操作操作系统,也会使用本地方法,对于本地方法来说,不同操作系统的实现不太一样,但是使用起来是一样的。
2. Unsafe创建
public final class Unsafe {
// 单例对象
private static final Unsafe theUnsafe;
......
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
Unsafe类是单例实现,可以通过静态getUnsafe方法获取实例。但是有个前提是 在调用getUnsafe方法的时候会对调用者的ClassLoader进行检查,也就是检查类加载器,如果是由Bootstrap classLoader加载的,那么才可以获得实例 如果不是那么就抛出异常SecurityException,所以,只有启动类加载器加载的类才可以调用Unsafe类中的方法,有助于避免被不可信代码调用,(因为这个原因说一在获取Unsafe类的实例的时候 大概率会抛出SecurityException异常,这样就要有别的方法来获取实例,比如下面讲到的反射方法)
那么为什么使用Unsafe类这么有限制?
Unsafe类提供的功能很底层,它可以访问系统资源,操作系统资源,所以存在一些安全风险,
在这个限制的条件下如何来获取Unsafe实例呢
我知道的方法是一个:
通过反射
我们可以通过反射来获得Unsafe类中以及完成实例化的theUnsafe对象
//通过反射获取Unsafe类中已经实例化完成的theUnsafe对象
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {//处理异常
e.printStackTrace();
return null;//返回内容
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
3. Unsafe功能
3.1内存操作
Java中不能直接操作内存,对象的分配内存和释放都是JVM完成的,在Unsafe中,提供了几个方法可以直接操作内存:
//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);
测试:
public void Test() {
Unsafe unsafe = reflectGetUnsafe();
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr1 = unsafe.reallocateMemory(addr,size*2);
System.out.println("addr:" + addr);
System.out.println("addr1:" + addr1);
try {
unsafe.setMemory(null,addr,size,(byte) 1);
for (int i = 0; i < 2; i++) {
//o为哪个对象 addr为对象中的偏移量(offset) o1是另一个对象 l1为拷贝到哪里 l1为拷贝字节
//进行了两次拷贝 每次拷贝将内存地址addr开始的四个字节拷贝到add1和add1+size*i(也就是4)的内存
unsafe.copyMemory(null, addr, null, addr1 + size * i, 4);
}
System.out.println(unsafe.getInt(addr1));
System.out.println(unsafe.getLong(addr1));
} finally {
//因为Unsafe的对象不会自动释放 所以要手动释放
unsafe.freeMemory(addr);
unsafe.freeMemory(addr1);
}
}
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
使用allocateMemory方法申请4字节长度的内存空间,调用setMemory方法在每个字节中写入byte类型的1,调用getInt方法的时候 取四个字节 就是00000001 00000001 00000001 00000001 十进制就是16843009
然后再代码中调用reallocateMemory 重新分配8字节的内存空间,在循环中 分别拷贝两次 每次拷贝内存地址addr的4个字节 拷贝到addr1和addr1+4的内存空间上。
此外由于这种分配是堆外内存,不能自动回收,所以要再finally中手动使用freeMemory释放。
使用堆外内存的好处:
- **对垃圾回收停顿改善,**因为推外内存不被JVM管理,所以在JVM垃圾回收的时候,可以减少垃圾回收停顿。
- **提高I/O操作性能,**会存在堆内内存到堆外内存的拷贝,所以如果使用堆外内存的话,就可以提高IO操作性能,可以将频繁需要拷贝的内存 或者 生命周期短的内存直接放在堆外,避免在堆内的时候一直拷贝到堆外去。
应用场景:
DirectByBuffer是Java实现堆外内存的一个重要类,在通信过程中做缓冲池,在NIO框架中使用较多,DirectByBuffer的堆外内存创建,使用,释放,都是由Unsafe提供的堆外内存API来实现的。
DirectByBuffer构造函数👇:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存并返回基地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 内存初始化
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
创建DirectByBuffer的时候,通过unsafe.allocateMemory进行内存分配,然后使用unsafe.setMemory进行内存初始化,使用Cleaner类的create方法创建对象,这个对象可以用来跟踪DirectByBuffer对象,如果这个对象被垃圾回收了,那么分配的堆外内存也释放。
3.2 内存屏障
计算机的运行时,编译器和cpu会在保证结果不变的前提下,进行代码重排序,提升性能,但是这样可能会导致CPU高速缓存的数据和内存中的数据不一致,所以内存屏障的作用减少避免这样的事情发生。
内存屏障的实现在不同操作系统上可能不一样,所以Java引入3个内存屏障,都是统一由JVM实现的,屏蔽了底层的差异。
Unsafe中的桑个内存屏障:
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
对内存屏障的理解,使用内存屏障之前,如果有123操作,那么123是可以打乱来执行的,但是有了内存屏障之后,它可以规定某个点,它让在某一点之前的所有读写任务都执行完以后才可以进行点后任务。 比如123操作 可能会规定 执行完12操作才可以进行3操作,3操作不可以提前到1或2之前。
用fullFence来说 它就规定了在这个屏障之前的所以读写操作执行完以后,才能进行屏障之后的操作。
并且完成屏障前的操作后,会将缓存数据设置为无效,重新从主存中获取,这个功能的用处就是,可以让内存屏障在多线程下实现可见性(内存屏障本身只有禁止指令重排序,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;
}
}
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的变化,因为在Java内存模型中,运行的线程不是直接读取主存中的变量,而是修改线程本身的工作内存的变量,然后传给主内存的,并且线程和线程之间的内存是独立的,如果主线程要感知flag变化,那么就应该借助于主内存,主内存将修改后的flag同步给主线程,因为loadFence屏障之前的线程执行结束了,将缓存数据设置为无效,然后重新读取最新的主存内容,只有在合适的地方加入屏障,才可以保证内存可见性。
!!!保证内存可见性是因为它会立刻刷新屏障前的数据到主存中去,不保证内存可见性是因为一个线程写入的数据 不一定会被另一个线程立刻可见,而屏障就是保证了让他立刻可见后,才让另一个线程拿到正确的数据。
应用场景:
Java8中的一种锁机制——StampedLock,读写锁的一个改进版本,是一种乐观读锁,不会阻塞写线程获取写锁,缓解读多写少的时候,有的线程用的少的情况,但是因为不阻塞写锁,所以线程共享变量从主存到线程内存的时候,会有数据的不一样。
解决方法,stampedLock的validate方法会用过Unsafe1的loadFence方法加入一个load内存屏障,来防止指令重排序(禁止读操作重排序,内存屏障前的读操作不能重排序到内存屏障之后)
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
3.3 对象操作
Unsafe提供了全部的8种基础数据类型以及Object的put和get方法,并且所由的put方法都可以直接修改内存种的数据。
Unsafe提供volatile读写和有序写入方法。
//在对象的指定偏移地址处读取一个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低,因为只保证有序写入,不保证可见性,也就是一个线程写入的值不能保证其他线程立刻可见,这是有关内存屏障的。
- Load:读,将主存种的数据拷贝到处理器的缓存中
- Store:写,将处理器中的缓存拷贝到主存中
!!内存屏障可以实现内存可见性和顺序写入的原因:
顺序写入和volatile写入的区别在于:
- 顺序写入是在写时加入内存屏障的类型为StoreStore类型
- 而volatile写入时加入的内存屏障类型时StoreLoad类型:
在顺序写入的时候使用StoreStore类型,可以保证Store1立刻刷新数据到主存中去,然后才可以进行Store2的后续操作。
在volatile写入的时候使用StoreLoad类型,保证Store1立刻刷新数据到主存中去,然后才可以进行Load2及后续操作,并且StoreLoad屏障会让在屏障之前的所有指令全部完成以后,才执行屏障之后的指令。
三个写入方法的效率:put>putOrder>putVolatile
对象实例化:
使用Unsafe的allocateInstance方法,进行非常规的对象实例化。
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
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());
}
应用场景:
- 常规对象实例化方法:使用new来创建对象,但是使用new的话,如果类只有一个有参构造方法也没有生命无参构造方法的时候,必须使用有参构造方法来构造对象。
- 非常规对象实例化方法:Unsafe中的allocateInstance方法,通过class对象就可以构造实例,不用调用构造方法,JVM安全检查,构造器是private修饰的 也可以实例化,这个allocateInstance在Gson(反序列化)中也有用到过。
3.4 数组操作
arrayBaseOffest和arrayIndexScale这两个方法配合使用,定位数字中每个元素在内存中的位置。
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
3.5 CAS操作
CAS:意思是比较和替换的意思,在并发编程中经常会用到CAS来保证数据的正确性,是一个原子操作,CAS中有三个参数(内存位置的值,预期值,新值),执行时会将内存位置的值和预期值比较,如果相同就会吧内存位置的值替换为新值,如果不同就不会替换。
Unsafe提供的CAS方法底层实现是CPU指令cmpxchg
在Unsafe类中提供了三种CAS类型:
compareAndSwapObject、compareAndSwapInt、compareAndSwapLong
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
应用场景:
在JUC包的并发工具类中使用了很多CAS操作,如synchronized和AQS都有使用。‘
比如compareAndSwapInt:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
其中o为要操作的对象,offset是偏移量,expected是预期值,x是替换的值,如果offset的值和expected的值一样,就会把offset这个字段的值更新为x。
使用1加到10的多线程环境下的例子:
import sun.misc.Unsafe;
public class CasTest {
// public static void main(String[] args) throws NoSuchFieldException {
// Field field = Unsafe.class.getField("theUnsafe");
// Demo1 demo1 = new Demo1(field);
// }
private void increment(int x) {
Unsafe unsafe = Demo.reflectGetUnsafe();
while (true) {
try {
//拿到全局变量的a值 初始为0
long offset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
//第一次 当offset(a=0)等于x-1(x=1)的时候 把x替换到offset上
if (unsafe.compareAndSwapInt(this,offset,x-1,x)){
break;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
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();
}
}
解释:
因为多线程环境下 第一个线程加到2的时候,第二个线程从5开始加,他进入到increment去进行CAS比较的时候会发现内存中的a值(a=2)并不是期望值(x-1=4),那么就不会执行CAS操作把替换值(x=5)给内存中的a值,就会一直循环下去知道a被加到了正确的值的时候,这时候if才会成立然后才break结束这一次任务。
3.6 线程调度
Unsafe类中提供了park,unpark,monitorEnter,monitorExit,tryMonitorEnter方法进行线程调度
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
park阻塞线程,unpark取消阻塞,monitorEnter获取对象锁,monitorExit释放对象锁,tryMonitorEnter尝试获得对象锁。
在Unsafe源码中 除了park和unpark 剩下的方法以及不建议使用了。
应用场景:
抽象队列同步器 AbstractQueuedSynchronizer(AQS)就是通过调用 LockSupport.park() 和 LockSupport.unpark() 实现线程阻塞和线程唤醒的,而其实他的park和unpark调用的是Unsafe类的park和unpark方法。
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);
}
对Unsafe的park和unpark方法测试👇
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Unsafe unsafe = Demo.reflectGetUnsafe();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("取消阻塞主线程");
unsafe.unpark(mainThread);//取消主线程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("阻塞主线程");
unsafe.park(false,0l);
System.out.println("取消成功");
}
阻塞主线程
取消阻塞主线程
取消成功
子线程运行时睡眠,主线程打印内容并且阻塞自己,子线程睡眠结数回复工作 打印内容以后唤醒主线程,主线程打印最后的内容。
3.7 Class操作
Unsafe对Class的相关操作主要包括类加载和静态变量的操作方法。
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
应用场景:
Lambda表达式需要实现依赖Unsafe的defineAnonymousClass方法定义实现相应的函数式接口的匿名类。
3.8 系统信息
有两个获取系统信息的方法
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
4. 总结
介绍了Unsafe的概念,功能有哪些,和使用的方法和场景,Unsafe可以让我们更底层的操作 操作系统,便捷的访问操作系统内存,管理操作系统内存资源,但是带来的安全隐患也是需要注意的,堆外内存的处理,指针安全的处理等等。