Java中的Random和ThreadLocalRandom详细解析
作者:魅Lemon
一、Random类
1、简介
Random 类用于生成伪随机数的流。 该类使用48位种子,其使用线性同余公式进行修改
- Math.random()使用起来相对更简单,但不是线程安全的;
- java.util.Random的Random类是线程安全的。 但是跨线程的同时使用java.util.Random实例可能会遇到争用,从而导致性能下降。 在多线程设计中考虑使用ThreadLocalRandom类;
- java.util.Random的Random不是加密安全的。 考虑使用SecureRandom获取一个加密安全的伪随机数生成器,供安全敏感应用程序使用
2、Random的构造函数
Random():创建一个新的随机数生成器
/** * Creates a new random number generator. This constructor sets * the seed of the random number generator to a value very likely * to be distinct from any other invocation of this constructor. */ public Random() { //System.nanoTime()返回正在运行的Java虚拟机的高分辨率时间源的当前值,以纳秒为单位。 //这里会调用有参构造函数 this(seedUniquifier() ^ System.nanoTime()); }
private static long seedUniquifier() { // 线性同余生成元表 for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; // 两个很大的数相乘 if (seedUniquifier.compareAndSet(current, next)) // 这个比较并且交换CAS // 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作! //如果不是就一直循环!就是为了保证即使在多线程的环境中返回的也是不同的数 return next; } } // atomic 这个是 juc 里面修饰的原子性的 long ,get方法说就是获得这个构造函数里面的值 private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
random(long seed):使用单个 Long 种子创建一个新的随机数生成器
伪随机使用了线性同余法(具体可自行查阅资料)
public Random(long seed) { if (getClass() == Random.class) this.seed = new AtomicLong(initialScramble(seed)); else { // 子类可能重写了这个不考虑 this.seed = new AtomicLong(); // 创建一个新的AtomicLong,初始值为 0 setSeed(seed); } } //清除nextGaussian()使用的haveNextNextGaussian 标志, synchronized public void setSeed(long seed) { this.seed.set(initialScramble(seed)); haveNextNextGaussian = false; } private static long initialScramble(long seed) { return (seed ^ multiplier) & mask; } private static final long multiplier = 0x5DEECE66DL; // x & [(1L << 48)–1]与 x(mod 2^48)等价 取低位48位 // 带符号左移 private static final long mask = (1L << 48) - 1;
3、next()核心方法
Random在多线程的环境是并发安全的,它解决竞争的方式是使用用原子类,本质上上也就是CAS + Volatile保证线程安全
在Random类中,有一个AtomicLong的域,用来保存随机种子。其中每次生成随机数时都会根据随机种子做移位操作以得到随机数。
//Long类型的随机 //long类型在Java中总弄64bit,对next方法的返回值左移32作为long的高位,然后将next方法返回值作为低32位,作为long类型的随机数。 //此处关键之处在于next方法 public long nextLong() { return ((long)(next(32)) << 32) + next(32); }
以下是next方法的核心,使用seed种子,不断生成新的种子,然后使用CAS将其更新,再返回种子的移位后值。这里不断的循环CAS操作种子,直到成功。可见,Random实现原理主要是利用随机种子采用一定算法进行处理生成随机数,在随机种子的安全保证利用原子类AtomicLong。
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)); return (int)(nextseed >>> (48 - bits)); }
4、Random在并发下的缺点
虽然Random是线程安全,但是对于并发处理使用原子类AtomicLong在大量竞争时,使用同一个 Random 对象可能会导致线程阻塞,由于很多CAS操作会造成失败,不断的Spin,而造成CPU开销比较大而且吞吐量也会下降。
这里可以自行多线程测试,可以发现随着线程增加,Random随着竞争越来越激烈,然后耗时越来越多。然而ThreadLocalRandom随着线程数的增加,基本没有变化。所以在大并发的情况下,随机的选择,可以考虑ThreadLocalRandom提升性能。
二、ThreadLocalRandom
1、简介
ThreadLocalRandom是Random的子类,它是将Seed随机种子隔离到当前线程的随机数生成器,从而解决了Random在Seed上竞争的问题,它的处理思想和ThreadLocal本质相同。
Unsafe 类内的方法透露着一股 “Unsafe” 的气息,具体表现就是可以直接操作内存,而不做任何安全校验,如果有问题,则会在运行时抛出 Fatal Error,导致整个虚拟机的退出。
2、原理分析
2.1 ThreadLocalRandom单例模式
从下述代码可以发现ThreadLocalRandom使用了单例模式,即在一个Java应用中只有一个ThreadLocalRandom对象。
当UNSAFE.getInt(Thread.currentThread(), PROBE)返回0时,就执行localInit(),最后返回单例。
// 一个java应用只有一个实例 // ThreadLocalRandom对象通过ThreadLocalRandom.current()获取,之后可以直接返回随机数 static final ThreadLocalRandom instance = new ThreadLocalRandom(); //获取ThreadLocalRandom对象实例 public static ThreadLocalRandom current() { if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; }
2.2 Seed随机种子隔离到当前线程
核心方法
//会从 object 对象var1的内存地址偏移var2后的位置读取四个字节作为long型返回 public native long getLong(Object var1, long var2); //可以将object对象var1的内存地址偏移var2后的位置后四个字节设置为 var4 public native void putLong(Object var1, long var2, long var4);
UNSAFE.getInt(Thread.currentThread(), PROBE)是获取当前Thread线程对象中的PROBE。
首先获取变量名SEED、PROBE等参数相对对象的偏移位置
// Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long SEED; private static final long PROBE; private static final long SECONDARY; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; SEED = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSeed")); PROBE = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomProbe")); SECONDARY = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSecondarySeed")); } catch (Exception e) { throw new Error(e); } }
当第一次调用ThreadLocalRandom.current()方法时当前线程检测到PROBE未初始化会调用localInit()方法进行初始化,并把当前线程的seed值和probe值存储在当前线程Thread对象内存地址偏移相对应变量的位置
static final void localInit() { int p = probeGenerator.addAndGet(PROBE_INCREMENT); int probe = (p == 0) ? 1 : p; // skip 0 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); Thread t = Thread.currentThread(); UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe); }
threadLocalRandomProbe用于表示当前线程Thread是否初始化,如果是非0,表示其已经初始化。
换句话说,该变量就是状态变量,用于标识当前线程Thread是否被初始化。
threadLocalRandomSeed从注释中也可以看出,它是当前线程的随机种子。
随机种子分散在各个Thread对象中,从而避免了并发时的竞争点。
3、nextSeed()核心方法
nextSeed()生成随机种子用来生成随机数序列
public long nextLong() { return mix64(nextSeed()); }
因为在初始化的时候已经存储了当前线程的seed值和probe值到相应线程对象内存地址的偏移位置,调用nextSeed()时直接从当前线程对象偏移位置处进行获取,并生成下一个随机数种子到该位置,同时使用了UNSAFE类方法,不同线程间不需要竞争获得seed值,因此可以可以将竞争点隔离
final long nextSeed() { Thread t; long r; // read and update per-thread seed UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); return r; }
三、总结
Random是Java中提供的随机数生成器工具类,但是在大并发的情况下由于其随机种子的竞争会导致吞吐量下降,从而引入ThreadLocalRandom它将竞争点隔离到每个线程中,从而消除了大并发情况下竞争问题,提升了性能。
并发竞争的整体优化思路:lock -> cas + volatile -> free lock
到此这篇关于Java中的Random和ThreadLocalRandom详细解析的文章就介绍到这了,更多相关Random和ThreadLocalRandom解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!