你代码里的 ThreadLocalRandom,真的安全吗?

沙海 2021年4月25日05:03:50Java评论50字数 4570阅读15分14秒阅读模式
摘要

速读摘要

速读摘要文章源自JAVA秀-https://www.javaxiu.com/20286.html

最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到jdk包里的Random类。使用CAS更新下一次随机的种子,可以想到,如果多个线程同时使用这个对象,就肯定会有一些线程执行CAS连续失败,进而导致线程阻塞。对象对象头的结构被打乱了,解析对象失败抛出了错误,更严重的问题是报错信息中没有类名行号等信息,在复杂项目中排查这种问题真如同大海捞针。不过Unsafe的其他方法可不一定像这一对方法一样,使用他们时可能需要注意另外的安全问题,之后有遇到再说。文章源自JAVA秀-https://www.javaxiu.com/20286.html

原文约 2870 | 图片 3 | 建议阅读 6 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/20286.html

你代码里的 ThreadLocalRandom,真的安全吗?

zhenbianshu 小哈学Java 文章源自JAVA秀-https://www.javaxiu.com/20286.html

点击上方蓝色“小哈学Java”,选择“设为星标”回复“资源”获取独家整理的学习资料!
文章源自JAVA秀-https://www.javaxiu.com/20286.html

前言

最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 jdk 包里的 Random 类。但出于对性能的极致追求,就考虑使用 ThreadLocalRandom 类进行优化,在查看 ThreadLocalRandom 实现的过程中,又追了下 Unsafe 有部分代码,整个流程下来,学到了不少东西,也通过搜索和提问解决了很多疑惑,于是总结成本文。文章源自JAVA秀-https://www.javaxiu.com/20286.html

Random 的性能问题

使用 Random 类时,为了避免重复创建的开销,我们一般将实例化好的 Random 对象设置为我们所使用服务对象的属性或静态属性,这在线程竞争不激烈的情况下没有问题,但在一个高并发的 web 服务内,使用同一个 Random 对象可能会导致线程阻塞。文章源自JAVA秀-https://www.javaxiu.com/20286.html

Random 的随机原理是对一个”随机种子”进行固定的算术和位运算,得到随机结果,再使用这个结果作为下一次随机的种子。在解决线程安全问题时,Random 使用 CAS 更新下一次随机的种子,可以想到,如果多个线程同时使用这个对象,就肯定会有一些线程执行 CAS 连续失败,进而导致线程阻塞。文章源自JAVA秀-https://www.javaxiu.com/20286.html

ThreadLocalRandom

jdk 的开发者自然考虑到了这个问题,在 concurrent 包内添加了 ThreadLocalRandom 类,第一次看到这个类名,我以为它是通过 ThreadLocal 实现的,进而想到恐怖的内存泄漏问题,但点进源码却没有 ThreadLocal 的影子,而是存在着大量 Unsafe 相关的代码。文章源自JAVA秀-https://www.javaxiu.com/20286.html

我们来看一下它的核心代码:文章源自JAVA秀-https://www.javaxiu.com/20286.html

UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);文章源自JAVA秀-https://www.javaxiu.com/20286.html

翻译成更直观的 Java 代码就像:文章源自JAVA秀-https://www.javaxiu.com/20286.html

Thread t = Thread.currentThread();long r = UNSAFE.getLong(t, SEED) + GAMMA;UNSAFE.putLong(t, SEED, r);

看上去非常眼熟,像我们平常往 Map 里 get/set 一样,以 Thread.currentThread() 获取到的当前对象里 key,以 SEED 随机种子作为 value。文章源自JAVA秀-https://www.javaxiu.com/20286.html

但是以对象作为 key 是可能会造成内存泄漏的啊,由于 Thread 对象可能会大量创建,在回收时不 remove Map 里的 value 时会导致 Map 越来越大,最后内存溢出。文章源自JAVA秀-https://www.javaxiu.com/20286.html

Unsafe

功能

不过再仔细看 ThreadLocalRandom 类的核心代码,发现并不是简单的 Map 操作,它的 getLong() 方法需要传入两个参数,而 putLong() 方法需要三个参数,查看源码发现它们都是 native 方法,我们看不到具体的实现。两个方法签名分别是:文章源自JAVA秀-https://www.javaxiu.com/20286.html

public native long getLong(Object var1, long var2);public native void putLong(Object var1, long var2, long var4);

虽然看不到具体实现,但我们可以查得到它们的功能,下面是两个方法的功能介绍:文章源自JAVA秀-https://www.javaxiu.com/20286.html

  • putLong(object, offset, value) 可以将 object 对象内存地址偏移 offset 后的位置后四个字节设置为 value。文章源自JAVA秀-https://www.javaxiu.com/20286.html

  • getLong(object, offset) 会从 object 对象内存地址偏移 offset 后的位置读取四个字节作为 long 型返回。文章源自JAVA秀-https://www.javaxiu.com/20286.html

不安全性

作为 Unsafe 类内的方法,它也透露着一股 “Unsafe” 的气息,具体表现就是可以直接操作内存,而不做任何安全校验,如果有问题,则会在运行时抛出 Fatal Error,导致整个虚拟机的退出。文章源自JAVA秀-https://www.javaxiu.com/20286.html

在我们的常识里,get 方法是最容易抛异常的地方,比如空指针、类型转换等,但 Unsafe.getLong() 方法是个非常安全的方法,它从某个内存位置开始读取四个字节,而不管这四个字节是什么内容,总能成功转成 long 型,至于这个 long 型结果是不是跟业务匹配就是另一回事了。而 set 方法也是比较安全的,它把某个内存位置之后的四个字节覆盖成一个 long 型的值,也几乎不会出错。文章源自JAVA秀-https://www.javaxiu.com/20286.html

那么这两个方法”不安全”在哪呢?文章源自JAVA秀-https://www.javaxiu.com/20286.html

它们的不安全并不是在这两个方法执行期间报错,而是未经保护地改变内存,会引起别的方法在使用这一段内存时报错。文章源自JAVA秀-https://www.javaxiu.com/20286.html

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)”。文章源自JAVA秀-https://www.javaxiu.com/20286.html

可以从报错信息中看到虚拟机因为这个 fatal error abort 退出了,原因也很简单,我使用 unsafe 将 Test 类 value 属性的位置设置成了 long 型值 2333,而当我使用 value 属性时,虚拟机会将这一块内存解析为 String 对象,原 String 对象对象头的结构被打乱了,解析对象失败抛出了错误,更严重的问题是报错信息中没有类名行号等信息,在复杂项目中排查这种问题真如同大海捞针。文章源自JAVA秀-https://www.javaxiu.com/20286.html

不过 Unsafe 的其他方法可不一定像这一对方法一样,使用他们时可能需要注意另外的安全问题,之后有遇到再说。文章源自JAVA秀-https://www.javaxiu.com/20286.html

ThreadLocalRandom 的实现

那么 ThreadLocalRandom 是不是安全的呢,再回过头来看一下它的实现。文章源自JAVA秀-https://www.javaxiu.com/20286.html

ThreadLocalRandom 的实现需要 Thread 对象的配合,在 Thread 对象内存在着一个属性 threadLocalRandomSeed,它保存着这个线程专属的随机种子,而这个属性在 Thread 对象的 offset,是在 ThreadLocalRandom 类加载时就确定了的,具体方法是 SEED = UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("threadLocalRandomSeed"));文章源自JAVA秀-https://www.javaxiu.com/20286.html

我们知道一个对象所占用的内存大小在类被加载后就确定了的,所以使用 Unsafe.objectFieldOffset(class, fieldName) 可以获取到某个属性在类中偏移量,而在找对了偏移量,又能确定数据类型时,使用 ThreadLocalRandom 就是很安全的。文章源自JAVA秀-https://www.javaxiu.com/20286.html

疑问

在查找这些问题的过程中,我也产生了两个疑问点。文章源自JAVA秀-https://www.javaxiu.com/20286.html

使用场景

首先就是 ThreadLocalRandom 为什么非要使用 Unsafe 来修改 Thread 对象内的随机种子呢,在 Thread 对象内添加 get/set 方法不是更方便吗?文章源自JAVA秀-https://www.javaxiu.com/20286.html

stackOverFlow 上有人跟我同样的疑问,why is threadlocalrandom implemented so bizarrely,被采纳的答案里解释说,对 jdk 开发者来说 Unsafe 和 get/set 方法都像普通的工具,具体使用哪一个并没有一个准则。文章源自JAVA秀-https://www.javaxiu.com/20286.html

这个答案并没有说服我,于是我另开了一个问题,里面的一个评论我比较认同,大意是 ThreadLocalRandom 和 Thread 不在同一个包下,如果添加 get/set 方法的话,get/set 方法必须设置为 public,这就有违了类的封闭性原则。文章源自JAVA秀-https://www.javaxiu.com/20286.html

内存布局

另一个疑问是我看到 Unsafe.objectFieldOffset 可以获取到属性在对象内存的偏移量后,自己在 IDEA 里使用 main 方法试了上文中提到的 Test 类,发现 Test 类的唯一一个属性 value 相对对象内存的偏移量是 12,于是比较疑惑这 12 个字节的组成。文章源自JAVA秀-https://www.javaxiu.com/20286.html

我们知道,Java 对象的对象头是放在 Java 对象的内存起始处的,而一个对象的 MarkWord 在对象头的起始处,在 32 位系统中,它占用 4 个字节,而在 64 位系统中它占用 8 个字节,我使用的是 64 位系统,这毫无疑问会占用 8 个字节的偏移量。文章源自JAVA秀-https://www.javaxiu.com/20286.html

紧跟 MarkWord 的应该是 Test 类的类指针和数组对象的长度,数组长度是 4 字节,但 Test 类并非数组,也没有其他属性,数据长度可以排除,但在 64 位系统下指针也应该是 8 字节的啊,为什么只占用了 4 个字节呢?文章源自JAVA秀-https://www.javaxiu.com/20286.html

唯一的可能性是虚拟机启用了指针压缩,指针压缩只能在 64 位系统内启用,启用后指针类型只需要占用 4 个字节,但我并没有显示指定过使用指针压缩。查了一下,原来在 1.8 以后指针压缩是默认开启的,在启用时使用 -XX:-UseCompressedOops 参数后,value 的偏移量变成了 16。文章源自JAVA秀-https://www.javaxiu.com/20286.html

小结

在写代码时还是要多注意查看依赖库的具体实现,不然可能踩到意想不到的坑,而且多看看并没有坏处,仔细研究一下还能学到更多。文章源自JAVA秀-https://www.javaxiu.com/20286.html

来源 | https://zhenbianshu.github.io/文章源自JAVA秀-https://www.javaxiu.com/20286.html

1. 2020中国高校薪资排行榜出炉!2. 深入理解JDK动态代理3. 代码总是被嫌弃写的太烂?装上这个IDEA插件再试试!4. 求你了,GC 日志打印别再瞎配置了最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。文章源自JAVA秀-https://www.javaxiu.com/20286.html

谢谢支持哟 (*^__^*)文章源自JAVA秀-https://www.javaxiu.com/20286.html

继续阅读
速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
weinxin
资源分享QQ群
本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定