关于java:ThreadLocalRandom-是线程安全的吗

7次阅读

共计 4013 个字符,预计需要花费 11 分钟才能阅读完成。

起源:https://zhenbianshu.github.io

前言

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

但出于对性能的极致谋求,就思考应用 ThreadLocalRandom 类进行优化,在查看 ThreadLocalRandom 实现的过程中,又追了下 Unsafe 有局部代码,整个流程下来,学到了不少货色,也通过搜寻和发问解决了很多纳闷,于是总结老本文。

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

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 上有人跟我同样的疑难:

https://stackoverflow.com/que…

被驳回的答案里解释说,对 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。

小结

在写代码时还是要多留神查看依赖库的具体实现,不然可能踩到意想不到的坑,而且多看看并没有害处,认真钻研一下还能学到更多。

近期热文举荐:

1.600+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0