欢迎关注方志朋的博客,回复”666“获面试宝典

来源:zhenbianshu.github.io

  • 背景

  • ThreadLocalRandom

  • Unsafe

  • 疑问

  • 小结


背景


前言

最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 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 上有人跟我同样的疑问,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。

小结


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

热门内容:
  • 不满月薪12000辞职,跳槽直接进大厂,据说背了很多面试八股文?!

  • 再见了月薪3w的大后端,低代码开发已成气候!

  • 今天面了个阿里出来的大佬,见识到了基础天花板!

  • 发现一款好用到爆的数据库工具,被惊艳到了!

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

干掉Random:这个类已经成为获取随机数的王者相关推荐

  1. 干掉 Random!它已经成为获取随机数的王者

    点击关注公众号,回复"1024"获取2TB学习资源! 前言 最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 jdk 包里的 Random 类.但出于对性能的极致追 ...

  2. 【小家java】Java中Random ThreadLocalRandom 设置随机种子获取随机数精讲

    相关阅读 [小家java]java5新特性(简述十大新特性) 重要一跃 [小家java]java6新特性(简述十大新特性) 鸡肋升级 [小家java]java7新特性(简述八大新特性) 不温不火 [小 ...

  3. 多线程下获取随机数的王者-ThreadLocalRandom

    背景 前言 最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到 jdk 包里的 Random 类.但出于对性能的极致追求,就考虑使用 ThreadLocalRandom 类进行优化,在查 ...

  4. Random类——获取随机数

    1.Random: 作用:–用于产生一个随机数 使用步骤(和Scanner类似) –导包:import java.util.Random; –创建对象:Random r = new Random(); ...

  5. Java中获取随机数

    Java中已经帮我们写好了一个类叫Random,这个类就可以生成一个随机数. 使用步骤: 1.导包:Random 这个类在哪 import java.util.Random; 导包的动作必须出现在类定 ...

  6. 获取随机数的方式Random类对象的方法 java 0913

    获取随机数的方式Random类对象的方法 java 0913 Random类 实例化这个类得到一个对象 利用对象的方法获取整数 代码 import java.util.Random;public cl ...

  7. 集合--List集合练习--集合的嵌套、Random类获取随机数

    集合的嵌套 import java.util.Objects;public class Student {private String name;private int age;public Stud ...

  8. 数字游戏一个类A有一个成员变量v有一个初值100. 定义一个类对A类的成员变量v进行猜。如果大了则提示大了小了则提示小了。等于则提示猜测成功(利用Math类的random方法获取随机数)

    数字游戏一个类A有一个成员变量v有一个初值100. 定义一个类对A类的成员变量v进行猜.如果大了则提示大了小了则提示小了.等于则提示猜测成功(利用Math类的random方法获取随机数) /*数字游戏 ...

  9. 水仙花数+JAVA数据输入+珠穆朗玛峰+获取随机数之Random

    目录 JAVA数据输入总结 水仙花数 珠穆朗玛峰 获取随机数之Random java基础创作day02 2021年1月14日下午,晴.连着几天的晴,看起来好像会暖和一些,但是,我还是好冷 JAVA数据 ...

最新文章

  1. 【Groovy】Groovy 脚本调用 ( Groovy 脚本编译 | Groovy 脚本字节码文件分析 )
  2. java服务限流_SpringCloud微服务:Sentinel哨兵组件,管理服务限流和降级
  3. Django中使用和配置Redis数据库
  4. lacp静态和动态区别_lacp静态与动态区别
  5. Simulink之三相桥式半控整流电路
  6. 《如何更改其他程序ListView控件中某个Item的内容》
  7. [轉]function, new function, new Function
  8. 《剑指offer》面试题19——二叉树的镜像(C++)
  9. 官网下载mysql jar包
  10. 思考的力量—谈谈程序员成长背后的思考力
  11. lhgdialog的弹出框操作
  12. python实现自动点击桌面按钮_Python实现鼠标隔几秒自动点击电脑某区域
  13. 复试前联系导师的邮件怎么写?内附模板
  14. Hadoop项目结构及其主要作用
  15. linux服务器抓包分析,抓包分析SSL/TLS连接建立过程总结
  16. C++实现first集follow集
  17. clap与slap_slap
  18. 【5.12-5.13】实训Day-6Day-7记录
  19. php 限制字数_php 限制字数用法
  20. ChatPPT 国内首个AI自动生成PPT效率工具

热门文章

  1. Java工具类-转换字符编码
  2. 读Lodash源码——chunk.js
  3. jquery validate 详解一
  4. Java 理论与实践: 非阻塞算法简介——看吧,没有锁定!(转载)
  5. 深入浅出Pytorch:02 PyTorch基础知识
  6. Datawhale组队学习周报(第012周)
  7. 07 Scratch等级考试(一级)模拟题
  8. 上班摸鱼,刚刚发现在 VScode 中可玩魂斗罗,超级玛丽
  9. 专访陈天桥:把钱投给甘坐冷板凳的AI研究员
  10. Arm 十年重磅发布 v9 架构,不受 EAR 约束,未来将覆盖 3000 亿颗芯片