前沿技术早知道,弯道超车有希望

积累超车资本,从关注DD开始

图文编辑:xj

来源:https://zhenbianshu.github.io/2019/12/is_threadlocalrandom_safe.html

最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 JDK 包里的 Random 类。

但出于对性能的极致追求,就考虑使用 ThreadLocalRandom 类进行优化,顺便查看了一下 ThreadLocalRandom 的实现,理解了一下部分源码。

在学习的过程中也看到了一篇文章,感觉还不错,分享给大家看看,相互学习。

至于标题:你觉得这玩意真的安全吗?

先说结论:是真的安全!

Random 的性能问题

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

Random 的随机原理是对一个”随机种子”进行固定的算术和位运算,得到随机结果,再使用这个结果作为下一次随机的种子。

在解决线程安全问题时,Random 使用 CAS 更新下一次随机的种子,可以想到,如果多个线程同时使用这个对象,就肯定会有一些线程执行 CAS 连续失败,进而导致线程阻塞。

ThreadLocalRandom

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

我们来看一下它的核心代码:

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

翻译成更直观的 Java 代码就像:

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 越来越大,最后内存溢出。

如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

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:

https://stackoverflow.com/questions/40620026/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。

最后说一句

在写代码时还是要多注意查看依赖库的具体实现,不然可能踩到意想不到的坑,而且多看看并没有坏处,仔细研究一下还能学到更多。

好了,看到了这里了,转发、在看、点赞随便安排一个吧,要是你都安排上我也不介意。写文章很累的,需要一点正反馈。给各位读者朋友们磕一个了:

对了,我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。

推荐阅读

  • Apache Dubbo 高危漏洞通告

  • 好几天没戴工牌坐地铁了,受不了!

  • 裸辞之后自己在家接单是什么体验?

最近两周DD整理了一波面经,涵盖阿里、腾讯、头条等众多大厂的真实面经分享。最近打算跳槽的小伙伴可以点击下方,关注公众号“SpringForAll社区”,发送关键词“2022Java面经”获取完整PDF哦!

你觉得 ThreadLocalRandom 这玩意真的安全吗?相关推荐

  1. 【239期】面试官问:你觉得 ThreadLocalRandom 这玩意安全吗?

    点击上方"Java精选",选择"设为星标" 别问别人为什么,多问自己凭什么! 下方有惊喜,留言必回,有问必答! 每天 08:15 更新文章,每天进步一点点... ...

  2. 给iPhone开发虚拟定位是否真的违法???

    昨天,我发了一篇<郑重提醒:开发这玩意真的犯法,已经有程序员被抓了!>的文章,里面有一个表达不太恰当的地方我更正一下:判决之前应该叫涉嫌违法,不能叫违法.新闻中相关人员是被依法批准逮捕,目 ...

  3. 注解报错_Java中的注解使用:全面性的总结一下

    前话: 今天,我们又来聊一下注解的使用,做一下详细的解析,也介绍了自定义注解,请耐心往下看哟! 注解的介绍: 在2005年,sun公司推出了jdk1.5,同时推出的注解功能吸引了很多人的目光,使用注解 ...

  4. 买电脑主要看什么配置_买电脑最主要的注意事项其实是预算

    今年疫情的原因,很多孩子都在家上网课.因为用手机屏幕太小累眼睛,操作又不方便,功能又少.所以很多家长朋友都需要给孩子买一台电脑,用来上网课,或者是给孩子查题.很多人咨询我买电脑应该注意什么,网上和实体 ...

  5. SPOJ 1812 LCS2 - Longest Common Substring II (后缀自动机)【两种做法】

    SPOJ 1812 LCS2 - Longest Common Substring II (后缀自动机)[两种做法] 手动博客搬家: 本文发表于20181217 23:54:35, 原地址https: ...

  6. SPOJ 1812 LCS2 - Longest Common Substring II (后缀自动机、状压DP)

    手动博客搬家: 本文发表于20181217 23:54:35, 原地址https://blog.csdn.net/suncongbo/article/details/85058680 人生第一道后缀自 ...

  7. ASP.NET MVC网站学习问题积累(一)

    最近工作压力比较大,不得已开始自学C#.同时网站开发业务开展迫在眉睫,只能先从ASP.NET学起.回想一下,连C#和ASP.NET的关系都没有明白,就被赶鸭子上架了...我觉得这将是我工作以来最具有戏 ...

  8. 【Java】什么?你项目还在用Date表示时间?!日期类LocalDateTime的使用

    什么?你项目还在用Date表示时间?! 这都什么年代了,怎么还在用 Date来处理和表示时间! 别的先不说,我们先来看几个关于 Date用法的例子,这玩意真的好用吗? 一.我想新建一个表示" ...

  9. NET问答: C# 中是否有最高效的方式对大文件做 checksum ?

    咨询区 Dario: 我需要在多台机器间同步大文件,不过文件高达 6G,通常我都是每几周手工同步一次,考虑到文件的文件名经常变,为了检验一致性,我考虑使用 checksum 机制. 我的计划是在 源机 ...

最新文章

  1. 日常总结:自学操作系统基础的一些领悟
  2. css提取页面元素唯一性_一日一技:爬虫如何正确从网页中提取伪元素?
  3. bitset中_Find_first()与_Find_next()函数
  4. Win10+Ubuntu16.04/Ubuntu18.04双系统安装教程
  5. Google 修改 Chrome API,防止隐身模式检测
  6. PSD分层模板|解析垂直化内容电商页面设计
  7. 微课|玩转Python轻松过二级(2.1节):常用内置对象
  8. SQL字符串转换为数组
  9. 【手写数字识别】基于matlab GUI BP神经网络手写数字识别【含Matlab源码 868期】
  10. apache ii评分怎么评_apache ii评分多少分为危重患者
  11. shark恒破解笔记6-摆脱NAG
  12. 如何使用Better Zip软件的密码保护功能
  13. jira迁移问题解决(实践篇)
  14. 中国石油大学《计算机应用基础》第三次在线作业
  15. android+高德地图教程,Android高德地图开发(三)地图简单操作
  16. 眼球追踪如何预测头部追踪
  17. 数据中台之OneID (ID-Mapping)架构设计细节全解
  18. 前端知识体系思维导图
  19. Frontal Brain Lobe and Its Function额叶及其功能
  20. 你的计算机无法访问网络设置,局域网无法访问其他计算机怎么办

热门文章

  1. java.lang.IllegalArgumentException和org.apache.catalina.LifecycleException
  2. gevent queue应用1
  3. MySQL分库分表环境下全局ID生成方案
  4. 缓存淘汰算法之LRU
  5. golang ping go-ping库 简介
  6. http 三种认证方式 Basic Session Token 简介
  7. python:urllib2.URLError urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed
  8. metasploit快速入门(二)收集信息
  9. Windows上打开大文件的工具
  10. 关于STL中的map和hash_map