天津做网站比较好的公司,做国外的众筹网站有哪些,做气体检测仪的网站,天津网站建设公司哪家好Java魔法类 Unsafe 文章导读#xff1a;(约12015字#xff0c;阅读时间大约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源码中的并发工具类出现过很多次
… 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() {}CallerSensitivepublic 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和add1size*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和addr14的内存空间上。
此外由于这种分配是堆外内存不能自动回收所以要再finally中手动使用freeMemory释放。
使用堆外内存的好处
**对垃圾回收停顿改善**因为推外内存不被JVM管理所以在JVM垃圾回收的时候可以减少垃圾回收停顿。**提高I/O操作性能**会存在堆内内存到堆外内存的拷贝所以如果使用堆外内存的话就可以提高IO操作性能可以将频繁需要拷贝的内存 或者 生命周期短的内存直接放在堆外避免在堆内的时候一直拷贝到堆外去。
应用场景 DirectByBuffer是Java实现堆外内存的一个重要类在通信过程中做缓冲池在NIO框架中使用较多DirectByBuffer的堆外内存创建使用释放都是由Unsafe提供的堆外内存API来实现的。
DirectByBuffer构造函数
DirectByteBuffer(int cap) { // package-privatesuper(-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 boundaryaddress 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 flagfalse;Overridepublic 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屏障会让在屏障之前的所有指令全部完成以后才执行屏障之后的指令。 三个写入方法的效率putputOrderputVolatile
对象实例化 使用Unsafe的allocateInstance方法进行非常规的对象实例化。
Data
public class A {private int b;public A(){this.b 1;}
}
public void objTest() throws Exception{A a1new 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值 初始为0long offset unsafe.objectFieldOffset(CasTest.class.getDeclaredField(a));//第一次 当offset(a0)等于x-1(x1)的时候 把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值(a2)并不是期望值(x-14)那么就不会执行CAS操作把替换值(x5)给内存中的a值就会一直循环下去知道a被加到了正确的值的时候这时候if才会成立然后才break结束这一次任务。
3.6 线程调度
Unsafe类中提供了parkunparkmonitorEntermonitorExittryMonitorEnter方法进行线程调度
//取消阻塞线程
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 系统信息
有两个获取系统信息的方法
//返回系统指针的大小。返回值为432位系统或 864位系统。
public native int addressSize();
//内存页的大小此值为2的幂次方。
public native int pageSize();
4. 总结
介绍了Unsafe的概念功能有哪些和使用的方法和场景Unsafe可以让我们更底层的操作 操作系统便捷的访问操作系统内存管理操作系统内存资源但是带来的安全隐患也是需要注意的堆外内存的处理指针安全的处理等等。