干掉Random:这个类才是获取随机数的王者!
Posted xhmj12
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干掉Random:这个类才是获取随机数的王者!相关的知识,希望对你有一定的参考价值。
上一篇:程序员裸辞全职接单一个月的感触
前言
最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 jdk 包里的 Random 类。但出于对性能的极致追求,就考虑使用 ThreadLocalRandom 类进行优化,在查看 ThreadLocalRandom 实现的过程中,又追了下 Unsafe 有部分代码,整个流程下来,学到了不少东西,也通过搜索和提问解决了很多疑惑,于是总结成本文。
Random 的性能问题
ThreadLocalRandom
jdk 的开发者自然考虑到了这个问题,在 concurrent 包内添加了 ThreadLocalRandom 类,第一次看到这个类名,我以为它是通过 ThreadLocal 实现的,进而想到恐怖的内存泄漏问题,但点进源码却没有 ThreadLocal 的影子,而是存在着大量 Unsafe 相关的代码。
UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);
翻译成更直观的 Java 代码就像:搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
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 越来越大,最后内存溢出。
功能
不过再仔细看 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)”。
相关阅读:2T架构师学习资料干货分享
不过 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 方法不是更方便吗?
内存布局
另一个疑问是我看到 Unsafe.objectFieldOffset 可以获取到属性在对象内存的偏移量后,自己在 IDEA 里使用 main 方法试了上文中提到的 Test 类,发现 Test 类的唯一一个属性 value 相对对象内存的偏移量是 12,于是比较疑惑这 12 个字节的组成。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
唯一的可能性是虚拟机启用了指针压缩,指针压缩只能在 64 位系统内启用,启用后指针类型只需要占用 4 个字节,但我并没有显示指定过使用指针压缩。查了一下,原来在 1.8 以后指针压缩是默认开启的,在启用时使用 -XX:-UseCompressedOops 参数后,value 的偏移量变成了 16。
小结
在写代码时还是要多注意查看依赖库的具体实现,不然可能踩到意想不到的坑,而且多看看并没有坏处,仔细研究一下还能学到更多。
以上是关于干掉Random:这个类才是获取随机数的王者!的主要内容,如果未能解决你的问题,请参考以下文章