文章目录
一、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
参考文章
https://mp.weixin.qq.com/s/f-lfqEUNvY6XRmCL-h0Qfg
https://www.cnblogs.com/lxyit/p/12654374.html
https://blog.csdn.net/tyh18226568070/article/details/105884912