《 面试又翻车了》这次竟然和 Random 有关?

2024-02-10 22:18

本文主要是介绍《 面试又翻车了》这次竟然和 Random 有关?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

小强最近面试又翻车了,然而令他郁闷的是,这次竟然是栽到了自己经常在用的 Random 上......

面试问题

既然已经有了 Random 为什么还需要 ThreadLocalRandom?

正文

Random 是使用最广泛的随机数生成工具了,即使连 Math.random() 的底层也是用 Random 实现的Math.random() 源码如下:可以看出 Math.random() 直接指向了 Random.nextDouble() 方法。

Random 使用

这开始之前,我们先来了解一下 Random 的使用。

Random random = new Random();
for (int i = 0; i < 3; i++) {// 生成 0-9 的随机整数random.nextInt(10);
}

以上程序的执行结果为:

1

0

7

Random 源码解析

可以看出 Random 是通过 nextInt() 方法生成随机整数的,那他的底层的是如何实现的呢?我们来看他的实现源码:

/*** 源码版本:JDK 11*/
public int nextInt(int bound) {// 验证边界的合法性if (bound <= 0)throw new IllegalArgumentException(BadBound);// 根据老种子生成新种子int r = next(31);// 计算最大值int m = bound - 1;// 根据新种子计算随机数if ((bound & m) == 0)  // i.e., bound is a power of 2r = (int)((bound * (long)r) >> 31);else {for (int u = r;u - (r = u % bound) + m < 0;u = next(31));}return r;
}

从以上源码我们可以看出,整个源码最核心的部分有两块:

  1. 根据老种子生成新种子;

  2. 根据新种子计算出随机数。

根据新种子计算出随机数的代码已经很明确了,我们需要确认一下 next() 方法是如何实现的,继续看源码:

/*** 源码版本:JDK 11*/
protected int next(int bits) {// 声明老种子和新种子long oldseed, nextseed;AtomicLong seed = this.seed;do {// 获取原子变量种子的值oldseed = seed.get();// 根据当前种子计算出新种子的值nextseed = (oldseed * multiplier + addend) & mask;} while (!seed.compareAndSet(oldseed, nextseed)); // 使用 CAS 更新种子return (int)(nextseed >>> (48 - bits));
}

根据以上源码可以看出,在使用老种子去获取新种子的时候,如果是多线程操作,则同一时刻只会有一个线程 CAS (Conmpare And Swap,比较并交换) 成功,其他失败的线程会通过自旋等待获取新种子,因此会有一定的性能消耗

当多线程使用同一个老种子来 CAS 的时候,只能有一个线程能够成功,而其他失败的线程只能通过自旋等待,这也是为什么 JDK 1.7 会引入 ThreadLocalRandom 的答案了,它主要为了提升多线程情况下 Random 的执行效率。

ThreadLocalRandom 使用

我们先来看 ThreadLocalRandom 的类关系图:可以看出 ThreadLocalRandom 继承于 Random 类,先来看它的使用:

ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 3; i++) {// 生成 0-9 的随机数System.out.println(threadLocalRandom.nextInt(10));
}

以上程序的执行结果为:

1

7

5

可以看出 ThreadLocalRandom 和 Random 一样,都是通过 nextInt() 方法实现随机整数生成的。

ThreadLocalRandom 源码解析

接下来我们来看 ThreadLocalRandom 的随机数是如何生成的,源码如下:

/*** 源码版本:JDK 11*/
public int nextInt(int bound) {if (bound <= 0)throw new IllegalArgumentException(BAD_BOUND);// 根据老种子生成新种子int r = mix32(nextSeed());int m = bound - 1;// 根据新种子计算算出随机数if ((bound & m) == 0) // power of twor &= m;else { // reject over-represented candidatesfor (int u = r >>> 1;u + m - (r = u % bound) < 0;u = mix32(nextSeed()) >>> 1);}return r;
}

从以上源码可以看出 ThreadLocalRandom 的 nextInt() 和 Random 的 nextInt() 在写法和实现思路都很像,他们主要的区别在 nextSeed() 方法上,源码如下:

/*** 源码版本:JDK 11*/
final long nextSeed() {Thread t; long r; // read and update per-thread seed// 把当前线程作为参数生成一个新种子U.putLong(t = Thread.currentThread(), SEED,r = U.getLong(t, SEED) + GAMMA);return r;
}
@HotSpotIntrinsicCandidate
public native void putLong(Object o, long offset, long x);

从以上源码可以看出,ThreadLocalRandom 并不是像 Thread 那样使用 CAS 和自旋来获取新种子,而是在每个线程中使用每个线程中保存自己的老种子来生成新种子,因此就可以避免多线程竞争和自旋等待的时间,所以在多线程环境下性能更高。

ThreadLocalRandom 注意事项

在使用 ThreadLocalRandom 时需要注意一下,在多线程不能共享一个 ThreadLocalRandom 对象,否则会造成生成的随机数都相同,如下代码所示:

// 声明多线程
ExecutorService service = Executors.newCachedThreadPool();
// 共享 ThreadLocalRandom
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {// 多线程执行随机数并打印结果service.submit(() -> {System.out.println(Thread.currentThread().getName() + ":" + threadLocalRandom.nextInt(10));;});
}

以上程序执行结果如下:

pool-1-thread-2:4

pool-1-thread-1:4

pool-1-thread-3:4

pool-1-thread-10:4

pool-1-thread-6:4

pool-1-thread-7:4

pool-1-thread-4:4

pool-1-thread-9:4

pool-1-thread-8:4

pool-1-thread-5:4

Random VS ThreadLocalRandom

Random 生成获取新种子,如下图所示:

ThreadLocalRandom 生成获取新种子,如下图所示:

性能对比

接下来我们使用 Oracle 官方提供的性能测试工具 JMH (Java Microbenchmark Harness,JAVA 微基准测试套件),来测试一下 Random 和 ThreadLocalRandom 的吞吐量(单位时间内成功执行程序的数量):

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;/*** JDK:11* Windows 10 I5-4460/16G*/
@BenchmarkMode(Mode.Throughput) // 测试类型:吞吐量
//@Threads(16)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class RandomExample {public static void main(String[] args) throws RunnerException {// 启动基准测试Options opt = new OptionsBuilder().include(RandomExample.class.getSimpleName()) // 要导入的测试类.warmupIterations(5) // 预热 5 轮.measurementIterations(10) // 度量10轮.forks(1).build();new Runner(opt).run(); // 执行测试}/*** Random 性能测试*/@Benchmarkpublic void randomTest() {Random random = new Random();for (int i = 0; i < 10; i++) {// 生成 0-9 的随机数random.nextInt(10);}}/*** ThreadLocalRandom 性能测试*/@Benchmarkpublic void threadLocalRandomTest() {ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();for (int i = 0; i < 10; i++) {threadLocalRandom.nextInt(10);}}
}

测试结果如下:其中,Cnt 表示运行了多少次,Score 表示执行的成绩,Units 表示每秒的吞吐量

从 JMH 测试的结果可以看出,ThreadLocalRandom 在并发情况下的吞吐量约是 Random 的 5 倍

完整基准测试代码下载:https://github.com/vipstone/blog-example/blob/master/blog-example/src/main/java/com/example/RandomExample.java

总结

本文讲了 Random 和 ThreadLocalRandom 的使用以及源码分析,Random 是通过 CAS 和自旋的方式生成随机数,在多线程模式下同一时刻只能有一个线程通过 CAS 获取到新种子并生成随机数,其他线程只能自旋等待,所以有一定的性能损耗。而在 JDK 1.7 时新增了 ThreadLocalRandom 它的种子保存在各自的线程中,因此不会有自旋等待的过程,所以高并发情况下性能更优秀。

最后,我们通过官方提供的基准测试工具 JMH 得到的结果,ThreadLocalRandom 的性能大约是 Random 的 5 倍,所以在高并发情况下尽量使用 ThreadLocalRandom。

参考 & 鸣谢 《Java 并发编程之美》翟陆续

原创不易,期待你的素质三连,ღ( ´・ᴗ・` )比心~

【END】

近期热文

 
  • 因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!

  • 有人说:轻量级锁一定比重量级锁快!我忍不住笑了

  • 如何模拟线程池溢出?线程池溢出后会怎样?「视频版」

关注下方二维码,订阅更多精彩内容

朕已阅 

这篇关于《 面试又翻车了》这次竟然和 Random 有关?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/698162

相关文章

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

java面试常见问题之Hibernate总结

1  Hibernate的检索方式 Ø  导航对象图检索(根据已经加载的对象,导航到其他对象。) Ø  OID检索(按照对象的OID来检索对象。) Ø  HQL检索(使用面向对象的HQL查询语言。) Ø  QBC检索(使用QBC(Qurey By Criteria)API来检索对象。 QBC/QBE离线/在线) Ø  本地SQL检索(使用本地数据库的SQL查询语句。) 包括Hibern

贝壳面试:什么是回表?什么是索引下推?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题: 1.谈谈你对MySQL 索引下推 的认识? 2.在MySQL中,索引下推 是如何实现的?请简述其工作原理。 3、说说什么是 回表,什么是 索引下推 ? 最近有小伙伴在面试 贝壳、soul,又遇到了相关的

毕业前第二次面试的感慨

距面试已经过去了有几天了,我现在想起来都有说多的恨感慨。 我一直都是想找刚刚起步的企业,因为这能让我学到更多的东西,然而正好有一家企业是刚起步的,而且他还有自己的产品专利,可以说这是一家,即是创业又是刚起步的公司,这家公司回复了我投给他的简历,这家企业想进一步了解我的情况,因为简历上我符合这家企业的基本要求,所以要进一步了解。 虽然面试的过程中,他给我的面试题,我做得并不是很理想,

腾讯社招面试经历

前提:本人2011年毕业于一个普通本科,工作不到2年。   15号晚上7点多,正在炒菜做饭,腾讯忽然打电话来问我对他们的Linux C++的职位是否感兴趣,我表达了我感兴趣之后,就开始了一段简短的电话面试,电话面试主要内容:C++和TCP socket通信的一些基础知识。之后就问我一道算法题:10亿个整数,随机生成,可重复,求最大的前1万个。当时我一下子就蒙了,没反应过来,何况我还正在烧

完整的腾讯面试经过

从9月10号开始到现在快两个月了,两个多月中,我经历数次面试和笔试,在经历这些的同时积累了不少的经验,也学到了不少东西,在此把它记录下来,算是和一起找工作中的同学一起共勉吧。我是本校的学生,专业是机械制造及其自动化,找工作的主要目标是计算机软件类和机械制造方向的国内的企业,所以意向去外企的同学就不必浪费时间看这些面经啦,想去国内IT企业的同学可以继续看下去。本贴中我把最近的腾讯面试经过写下

仕考网:结构化面试流程介绍

(一)结构化面试 结构化面试,也叫做标准化面试,考官按照预先设定好的一套试题以问答方式与应试者当面交谈,根据应试者的言语、行为表现,对其相关能力和个性特征作出相应评价。 (二)考试流程 抵达考场——审核抽签——面试候考——进入考场——面试答题——考生退场——计分审核 (三)答题技巧 1.声音洪亮,音量可以比平时说话声音大一点。 2.语速不要过快,语速快容易卡顿,而且不便于考官听清答

嵌入式面试经典30问:二

1. 嵌入式系统中,如何选择合适的微控制器或微处理器? 在嵌入式系统中选择合适的微控制器(MCU)或微处理器(MPU)时,需要考虑多个因素以确保所选组件能够满足项目的具体需求。以下是一些关键步骤和考虑因素: 1.1 确定项目需求 性能要求:根据项目的复杂度、处理速度和数据吞吐量等要求,确定所需的处理器性能。功耗:评估系统的功耗需求,选择低功耗的MCU或MPU以延长电池寿命或减少能源消耗。成本

Leetcode面试经典150题-128.最长连续序列-递归版本另解

之前写过一篇这个题的,但是可能代码比较复杂,这回来个简洁版的,这个是递归版本 可以看看之前的版本,两个版本面试用哪个都保过 解法都在代码里,不懂就留言或者私信 class Solution {/**对于之前的解法,我现在提供一共更优的解,但是这种可能会比较难懂一些(思想方面)代码其实是很简洁的,总体思想如下:不需要排序直接把所有数放入map,map的key是当前数字,value是当前数开始的