前言
最近在写一些业务代码时遇到一个须要产生随机数的场景,这时天然想到 jdk 包里的 Random 类。但出于对性能的极致追求,就考虑使用 ThreadLocalRandom 类进行优化,在查看 ThreadLocalRandom 实现的过程当中,又追了下 Unsafe 有部分代码,整个流程下来,学到了很多东西,也经过搜索和提问解决了不少疑惑,因而总结成本文。java
Random 的性能问题
使用 Random 类时,为了不重复建立的开销,咱们通常将实例化好的 Random 对象设置为咱们所使用服务对象的属性或静态属性,这在线程竞争不激烈的状况下没有问题,但在一个高并发的 web 服务内,使用同一个 Random 对象可能会致使线程阻塞。web
Random 的随机原理是对一个”随机种子”进行固定的算术和位运算,获得随机结果,再使用这个结果做为下一次随机的种子。在解决线程安全问题时,Random 使用 CAS 更新下一次随机的种子,能够想到,若是多个线程同时使用这个对象,就确定会有一些线程执行 CAS 连续失败,进而致使线程阻塞。数组
ThreadLocalRandom
jdk 的开发者天然考虑到了这个问题,在 concurrent 包内添加了 ThreadLocalRandom
类,第一次看到这个类名,我觉得它是经过 ThreadLocal 实现的,进而想到恐怖的内存泄漏问题,但点进源码却没有 ThreadLocal 的影子,而是存在着大量 Unsafe 相关的代码。安全
咱们来看一下它的核心代码:并发
UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);
复制代码
翻译成更直观的 Java 代码就像:dom
Thread t = Thread.currentThread();
long r = UNSAFE.getLong(t, SEED) + GAMMA;
UNSAFE.putLong(t, SEED, r);
复制代码
看上去很是眼熟,像咱们日常往 Map 里 get/set 同样,以 Thread.currentThread() 获取到的当前对象里 key,以 SEED 随机种子做为 value。高并发
可是以对象做为 key 是可能会形成内存泄漏的啊,因为 Thread 对象可能会大量建立,在回收时不 remove Map 里的 value 时会致使 Map 愈来愈大,最后内存溢出。工具
Unsafe
功能
不过再仔细看 ThreadLocalRandom 类的核心代码,发现并非简单的 Map 操做,它的 getLong() 方法须要传入两个参数,而 putLong() 方法须要三个参数,查看源码发现它们都是 native 方法,咱们看不到具体的实现。两个方法签名分别是:布局
public native long getLong(Object var1, long var2);
public native void putLong(Object var1, long var2, long var4);
复制代码
虽然看不到具体实现,但咱们能够查获得它们的功能,下面是两个方法的功能介绍:性能
- putLong(object, offset, value) 能够将 object 对象内存地址偏移 offset 后的位置后四个字节设置为 value。
- getLong(object, offset) 会从 object 对象内存地址偏移 offset 后的位置读取四个字节做为 long 型返回。
不安全性
做为 Unsafe 类内的方法,它也透露着一股 “Unsafe” 的气息,具体表现就是能够直接操做内存,而不作任何安全校验,若是有问题,则会在运行时抛出 Fatal Error,致使整个虚拟机的退出。
在咱们的常识里,get 方法是最容易抛异常的地方,好比空指针、类型转换等,但 Unsafe.getLong() 方法是个很是安全的方法,它从某个内存位置开始读取四个字节,而无论这四个字节是什么内容,总能成功转成 long 型,至于这个 long 型结果是否是跟业务匹配就是另外一回事了。而 set 方法也是比较安全的,它把某个内存位置以后的四个字节覆盖成一个 long 型的值,也几乎不会出错。
那么这两个方法”不安全”在哪呢?
它们的不安全并非在这两个方法执行期间报错,而是未经保护地改变内存,会引发别的方法在使用这一段内存时报错。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// Unsafe 设置了构造方法私有,getUnsafe 获取实例方法包私有,在包外只能经过反射获取
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// Test 类是一个随手写的测试类,只有一个 String 类型的测试类
Test test = new Test();
test.ttt = "12345";
unsafe.putLong(test, 12L, 2333L);
System.out.println(test.value);
}
复制代码
运行上面的代码会获得一个 fatal error,报错信息为 “A fatal error has been detected by the Java Runtime Environment: … Process finished with exit code 134 (interrupted by signal 6: SIGABRT)”。
能够从报错信息中看到虚拟机由于这个 fatal error abort 退出了,缘由也很简单,我使用 unsafe 将 Test 类 value 属性的位置设置成了 long 型值 2333,而当我使用 value 属性时,虚拟机会将这一块内存解析为 String 对象,原 String 对象对象头的结构被打乱了,解析对象失败抛出了错误,更严重的问题是报错信息中没有类名行号等信息,在复杂项目中排查这种问题真如同大海捞针。
不过 Unsafe 的其余方法可不必定像这一对方法同样,使用他们时可能须要注意另外的安全问题,以后有遇到再说。
ThreadLocalRandom 的实现
那么 ThreadLocalRandom 是否是安全的呢,再回过头来看一下它的实现。
ThreadLocalRandom 的实现须要 Thread 对象的配合,在 Thread 对象内存在着一个属性 threadLocalRandomSeed
,它保存着这个线程专属的随机种子,而这个属性在 Thread 对象的 offset,是在 ThreadLocalRandom 类加载时就肯定了的,具体方法是 SEED = UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("threadLocalRandomSeed"))
;
咱们知道一个对象所占用的内存大小在类被加载后就肯定了的,因此使用 Unsafe.objectFieldOffset(class, fieldName)
能够获取到某个属性在类中偏移量,而在找对了偏移量,又能肯定数据类型时,使用 ThreadLocalRandom 就是很安全的。
疑问
在查找这些问题的过程当中,我也产生了两个疑问点。
使用场景
首先就是 ThreadLocalRandom 为何非要使用 Unsafe 来修改 Thread 对象内的随机种子呢,在 Thread 对象内添加 get/set 方法不是更方便吗?
stackOverFlow 上有人跟我一样的疑问,why is threadlocalrandom implemented so bizarrely,被采纳的答案里解释说,对 jdk 开发者来讲 Unsafe 和 get/set 方法都像普通的工具,具体使用哪个并无一个准则。这个答案并无说服我,因而我另开了一个问题,里面的一个评论我比较认同,大意是 ThreadLocalRandom 和 Thread 不在同一个包下,若是添加 get/set 方法的话,get/set 方法必须设置为 public,这就有违了类的封闭性原则。
内存布局
另外一个疑问是我看到 Unsafe.objectFieldOffset 能够获取到属性在对象内存的偏移量后,本身在 IDEA 里使用 main 方法试了上文中提到的 Test 类,发现 Test 类的惟一一个属性 value 相对对象内存的偏移量是 12,因而比较疑惑这 12 个字节的组成。
咱们知道,Java 对象的对象头是放在 Java 对象的内存起始处的,而一个对象的 MarkWord 在对象头的起始处,在 32 位系统中,它占用 4 个字节,而在 64 位系统中它占用 8 个字节,我使用的是 64 位系统,这毫无疑问会占用 8 个字节的偏移量。
紧跟 MarkWord 的应该是 Test 类的类指针和数组对象的长度,数组长度是 4 字节,但 Test 类并不是数组,也没有其余属性,数据长度能够排除,但在 64 位系统下指针也应该是 8 字节的啊,为何只占用了 4 个字节呢?
惟一的可能性是虚拟机启用了指针压缩,指针压缩只能在 64 位系统内启用,启用后指针类型只须要占用 4 个字节,但我并无显示指定过使用指针压缩。查了一下,原来在 1.8 之后指针压缩是默认开启的,在启用时使用 -XX:-UseCompressedOops 参数后,value 的偏移量变成了 16。
小结
在写代码时仍是要多注意查看依赖库的具体实现,否则可能踩到意想不到的坑,并且多看看并无坏处,仔细研究一下还能学到更多。
看完三件事❤️
若是你以为这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
-
点赞,转发,有大家的 『点赞和评论』,才是我创造的动力。
-
关注公众号 『 java烂猪皮 』,不按期分享原创知识。
-
同时能够期待后续文章ing🚀
本文转自:枕边书的博客,做者:枕边书