内容简介:本文原创地址,
本文原创地址, 我的博客
: https://jsbintask.cn/2019/05/05/jdk/jdk8-unsafe/ (食用效果最佳),转载请注明出处!
简介
Unsafe
是jdk提供的一个直接访问操作系统资源的 工具 类(底层c++实现),它可以直接分配内存,内存复制,copy,提供cpu级别的 CAS
乐观锁等操作。它的目的是为了增强 java 语言直接操作底层资源的能力,无疑带来很多方便。但是,使用的同时就得额外小心!它的总体作用如下(图片来源网络):
Unsafe
位于sun.misc包下,jdk中的并发编程包juc(java.util.concurrent)基本全部靠 Unsafe
实现,由此可见其重要性。
基本使用
Unsafe被设计为单例,并且只允许被引导类加载器(BootstrapClassLoader)加载的类使用:
所以我们自己写的类是无法直接通过 Unsafe.getUnsafe()
获取的。当然,既然是java代码,我们就可以使用一点 歪道
,比如通过反射直接new一个或者将其内部静态成员变量 theUnsafe
获取出来:
public static void main(String[] args) throws Exception{ // method 1 Class<Unsafe> unsafeClass = Unsafe.class; Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor(); constructor.setAccessible(true); Unsafe unsafe1 = constructor.newInstance(); System.out.println(unsafe1); // method2 Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe2 = (Unsafe) theUnsafe.get(null); System.out.println(unsafe2); }
现在我们能够在自己代码里面使用Unsafe了,接下来看下它的使用以及jdk使用操作的。
CAS
CAS
译为Compare And Swap,它是乐观锁的一种实现。假设内存值为v,预期值为e,想要更新成得值为u,当且仅当内存值v等于预期值e时,才将v更新为u。 这样可以有效避免多线程环境下的同步问题。
在unsafe中,实现CAS算法通过cpu的原子指令 cmpxchg
实现,它对应的方法如下:
简单介绍下它使用的参数, var1
为内存中要操作的对象, var2
为要操作的值的内存地址偏移量, var4
为预期值, var5
为想要更新成的值。
为了方便理解,举个栗子。类User有一个成员变量name。我们new了一个对象User后,就知道了它在内存中的 起始值
,而成员变量name在对象中的位置偏移是固定的。这样通过这个起始值和这个偏移量就能够定位到name在内存中的具体位置。
所以我们现在的问题就是如何得出name在对象User中的偏移量,Unsafe自然也提供了相应的方法:
他们分别为获取静态成员变量,成员变量的方法,所以我们可以使用unsafe直接更新内存中的值:
public class UnsafeTest { public static void main(String[] args) throws Exception { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); User user = new User("jsbintask"); long nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name")); unsafe.compareAndSwapObject(user, nameOffset, "jsbintask1", "jsbintask2"); System.out.println("第一次更新后的值:" + user.getName()); unsafe.compareAndSwapObject(user, nameOffset, "jsbintask", "jsbintask2"); System.out.println("第二次更新后的值:" + user.getName()); } } class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } }
因为内存中name的值为”jsbintask”,而第一次使用 compareAndSwapObject
方法预期值为”jsbintask1”,这显然是不相等的,所以第一次更新失败,第二次我们传入了正确的预期值,更新成功!
如果我们分析juc包下的 Atomic
开头的原子类就会发现,它内部的原子操作全部来源于unsafe的CAS方法,比如AtomicInteger的getAndIncrement方法,内部直接调用unsafe的 getAndAddInt
方法,它的实现原理为:cas失败,就循环,直到成功为止,这就是我们所说的 自旋锁
!
内存分配
Unsafe还给我们提供了直接分配内存,释放内存,拷贝内存,内存设置等方法,值得注意的是,这里的内存指的是 堆外内存
!它是不受jvm内存模型掌控的,所以使用需要及其小心:
//分配内存, 相当于C++的malloc函数 public native long allocateMemory(long bytes); //释放内存 public native void freeMemory(long address); //在给定的内存块中设置值 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); //为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x);
我们可以写一段代码验证一下:
public static void main(String[] args) throws Exception { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 分配 10M的堆外内存 long _10M_Address = unsafe.allocateMemory(1 * 1024 * 1024 * 10); // 将10M内存的 前面1M内存值设置为10 unsafe.setMemory(_10M_Address, 1 * 1024 * 1024 * 1, (byte) 10); // 获取第1M内存的值: 10 System.out.println(unsafe.getByte(_10M_Address + 1000)); // 获取第1M内存后的值: 0(没有设置) System.out.println(unsafe.getByte(_10M_Address + 1 * 1024 * 1024 * 5)); }
我们分配了10M内存,并且将前1M内存的值设置为了10,取出了内存中的值进行比较,验证了unsafe的方法。
堆外内存不受jvm内存模型掌控,在nio(netty,mina)中大量使用对外内存进行管道传输,copy等,使用它们的好处如下:
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
- 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
而在jdk中,堆外内存对应的类为DirectByteBuffer
,它内部也是通过unsafe分配的内存:
这里值得注意的是,对外内存的回收借助了Cleaner
这个类。
线程调度
通过Unsafe还可以直接将某个线程挂起,这和调用 Object.wait()
方法作用是一样的,但是效率确更高!
我们熟知的AQS( AbstractQueuedSynchronizer
)内部挂起线程使用了 LockSupport
方法,而LockSupport内部依旧使用的是Unsafe:
我们同样可以写一段代码验证:
public static void main(String[] args) throws Exception { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i == 5) { // i == 5时,将当前线程挂起 unsafe.park(false, 0L); } System.out.println(Thread.currentThread().getName() + " printing i : " + i); } }, " Thread__Unsafe__1"); t1.start(); // 主线程休息三秒 Thread.sleep(3000L); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " printing i : " + i); if (i == 9) { // 将线程 t1 唤醒 unsafe.unpark(t1); } } System.in.read(); }
当线程t1运行到i=5时,被挂起,主线程执行,而主线程运行到i=9时,将t1唤醒,t1继续打印! 在park出debug可以观察t1线程的状态:
数组操作
对于数组,Unsafe提供了特别的方法返回不同类型数组在内存中的偏移量:
arrayBaseOffset
方法返回数组在内存中的偏移量,这个值是固定的。 arrayIndexScale
返回数组中的每一个元素的内存地址换算因子。举个栗子,double数组(注意不是包装类型)每个元素占用8个字节,所以换算因子为8,int类型则为4,通过这两个方法我们就能定位数组中每个元素的内存地址,从而赋值,下面代码演示:
public static void main(String[] args) throws Exception{ Class<Unsafe> unsafeClass = Unsafe.class; Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor(); constructor.setAccessible(true); Unsafe unsafe = constructor.newInstance(); Integer[] integers = new Integer[10]; // 打印数组的原始值 System.out.println(Arrays.toString(integers)); // 获取Integer数组在内存中的固定的偏移量 long arrayBaseOffset = unsafe.arrayBaseOffset(Integer[].class); System.out.println(unsafe.arrayIndexScale(Integer[].class)); System.out.println(unsafe.arrayIndexScale(double[].class)); // 将数组中第一个元素的更新为100 unsafe.putObject(integers, arrayBaseOffset, 100); // 将数组中第五个元素更新为50 注意 引用类型占用4个字节,所以内存地址 需要 4 * 4 = 16 unsafe.putObject(integers, arrayBaseOffset + 16, 50); // 打印更新后的值 System.out.println(Arrays.toString(integers)); }
我们通过获取Integer数组的内存偏移量,结合换算因子将第一个元素,第五个元素分别替换为了100,50。验证了我们的说法。
数组的原子操作,juc包也已经提供了相应的工具类,比如 AtomicIntegerArray
内部就是同过Unsafe的上述方法实现了数组的原子操作。
其它操作
Unsafe还提供了操作系统级别的方法如获取内存页的大小 public native int pageSize();
,获取系统指针大小 public native int addressSize();
jdk8还加入了新的方法,内存屏障,它的目的是为了防止指令重排序(编译器为了优化速度,会在保证单线程不出错的情况下将某些代码的顺序调换,比如先分配内存,或者先返回引用等,这样在多线程环境下就会出错):
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重 排序 到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence();
jdk1.8引入的 StampedLock
就是基于此实现的乐观读写锁.
另外,jdk1.8引入了lambda表达式,它其实会帮我们调用Unsafe的 public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
方法生成匿名内部类,如下面的代码:
public class UnsafeTest2 { public static void main(String[] args) { Function<String, Integer> function = Integer::parseInt; System.out.println(function.apply("100")); } }
查看字节码:
发现它调用了 LambdaMetafactory.metafactory
方法,最终调用了 InnerClassLambdaMetafactory
的spinInnerClass方法:
总结
通过反射可以获取Unsafe类的实例,他可以帮助我们进行堆外内存操作,内存copy,内存复制,线程挂起,提供了cpu级别的cas原子操作。另外还有lambda的匿名内部类的生成,数组内存操作等。juc包基本全部基于此类实现!
关注我,这里只有干货!
谢谢你支持我分享知识
扫码打赏,心意已收
打开 微信 扫一扫,即可进行扫码打赏哦
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
产品经理修炼之道
费杰 / 机械工业出版社华章公司 / 2012-7-30 / 59.00元
本书由资深产品经理、中国最大的产品经理沙龙Pmcaff创始人费杰亲自执笔,微软、腾讯、百度、新浪、搜狐、奇虎、阿里云、Evernote等国内外20余家大型互联网企业资深产品经理和技术专家联袂推荐。用系统化的方法论和丰富的实战案例解读了优秀产品经理所必须修炼的产品规划能力、产品设计能力、产品执行能力,以及思考、分析和解决问题的能力和方法,旨在为互联网产品经理打造核心竞争力提供实践指导。 全书一......一起来看看 《产品经理修炼之道》 这本书的介绍吧!