java.util.Random和ThreadLocalRandom
引言
ThreadLocalRandom类是从JDK7开始在并发包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。ThreadLocalRandom为后面要探讨的高并发累加器Striped64及其实现类提供了基础,所以先要理解它。本文首先分析Random类的源码以及它在多线程下使用的局限性,然后在通过分析ThreadLocalRandom源码及其解决办法。
java.util.Random分析
构造方法
在JDK7之前包括现在java.util.Random应该是使用比较广泛的随机数生成工具类,所以我们还是有必要了解其实现原理。首先来看看其构造方法和及其相关的方法:
1 public Random() {//默认的无参构造方法 2 this(seedUniquifier() ^ System.nanoTime()); 3 } 4 private final AtomicLong seed;//内部维护的用于产生随机数的种子 5 private static long seedUniquifier() { 6 // L'Ecuyer, "Tables of Linear Congruential Generators of 7 // Different Sizes and Good Lattice Structure", 1999 8 for (;;) { 9 long current = seedUniquifier.get(); 10 long next = current * 181783497276652981L; 11 if (seedUniquifier.compareAndSet(current, next)) 12 return next; 13 } 14 } 15 //原子变量 16 private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L); 17 //有参的构造方法 18 public Random(long seed) { 19 if (getClass() == Random.class) 20 this.seed = new AtomicLong(initialScramble(seed)); 21 else { 22 //子类可以重写setSeed()方法 23 this.seed = new AtomicLong(); 24 setSeed(seed); 25 } 26 } 27 private static long initialScramble(long seed) { 28 return (seed ^ multiplier) & mask; 29 } 30 synchronized public void setSeed(long seed) { 31 this.seed.set(initialScramble(seed)); 32 haveNextNextGaussian = false; 33 }
Random有两个构造方法, 有参的构造方法传入的long型参数和最终将要产生的随机数没有直接联系,它只是用来计算(initialScramble方法)出一个种子seed,或者我喜欢叫它随机因子,这个种子是用于将来产生随机数的起源数字,产生随机数的过程就是:1).先利用这个种子经过固定的计算公式得出一个新的种子数,并通过CAS成功更改这个种子(下一次产生随机数之前将会拿到这个被更新过的种子继续更新),2)通过这个被更改过的种子数再通过相应的公式最终产生随机数。由此可见参数相同的Random类将会产生相同的随机数,因为Random内部相关的计算过程都是固定的。不同的只是这个起始种子,它决定了将会产生的随机数,所以称之为“伪随机数”。
回到上面的源码,有参的构造方法传入了一个用于计算出种子的参数,那么无参的构造方法呢?由上面的源码可以看出,它最终还是调用的有参的构造方法,只是这个参数由一些计算得出,首先执行seedUniquifier(),其返回结果再和当前时间的纳秒值进行异或。seedUniquifier方法内部使用了AtomicLong+CAS的方式确保了多线程情况下不会产生相同的种子数,所以Random其实是线程安全的。当然在高并发下构造多个Random实例可能导致大量线程多次CAS重试。
另外,从有参的构造方法体可以看出,Random的子类其实可以重写设置种子数初始值的方法setSeed().
下面的例子不论在何时何地以何种方式运行,得到的结果都是相同的(17,88),这很好理解,因为当初始种子相同时,由于Random内部的计算过程都是固定不变的,所以将会产生的随机数也是确定的。只有使用无参的构造方法才会避免这种情况。
1 Random random = new Random(50); 2 System.out.println(random.nextInt(100));//总是17 3 System.out.println(random.nextInt(100));//总是88 4 Random random1 = new Random(50); 5 System.out.println(random1.nextInt(100));//也总是17 6 System.out.println(random1.nextInt(100));//也总是88
核心方法
接下来,虽然Random内部产生随机数的方法很多,不管是产生int,long,double等类型的随机数,其实都是利用了下面这个核心方法,当然它也是体现了Random局限性问题的根源:
1 protected int next(int bits) { 2 long oldseed, nextseed; 3 AtomicLong seed = this.seed; 4 do { 5 oldseed = seed.get(); 6 nextseed = (oldseed * multiplier + addend) & mask; 7 } while (!seed.compareAndSet(oldseed, nextseed)); 8 return (int)(nextseed >>> (48 - bits)); 9 }
该方法几乎是Random所有产生随机数的核心方法,参数bits表示将要产生的随机数的二进制数,例如int型是32,boolean是1, 该方法就是利用AtomicLong+CAS更新种子数,更新种子数的公式是先(oldseed * multiplier + addend) & mask,然后再进行移位算法>>>,据说这被称为所谓的“线性同余算法”,我们不需要理解它,只要知道这其实就是一个固定的计算公式,入参是旧种子数, 输出是新的种子数。在多线程下使用同一个Random实例生成随机数的时候,多个线程同时产生随机数之前计算新的种子的时候多个线程会竞争对同一个原子变量seed的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能的,这就是Random的局限性,所以ThreadLocalRandom应运而生。Random的这种AtomicLong+CAS机制虽然在高并发下效率低下,但是保障了高并发下不同线程产生不同的随机数,这也是线程安全的一种保障。
随机数的边界
Random提供的众多产生随机的方法 中,nextDouble(),nextFloat()是产生【0,1)之间的随机数,nextLong(),nextInt()产生的随机数分别是long(2的负64次方~2的64次方)和int(2的负32次方~2的32次方)型变量的边界,另外还有几个特殊方法:
- void nextBytes(byte[] bytes),生成内容随机的byte值,并放入bytes数组,注意如果传入的bytes数组如果有值,会被随机生成的byte覆盖。
- int nextInt(int bound),生成【0,bound)之间的随机数,该方法的实现虽然简短,但是据说是具有算法背景的高级工程师花了大量时间设计、实现的,千万不要企图使用Math.abs(rnd.nextInt()) % n这种方式去替代,你将会得到事与愿违的结果。
- double nextGaussian(),以上所有方法产生的随机数都是成均匀分布的,该方法取自此随机数生成器序列的、呈高斯(“正态”)分布的double值,其平均值是0.0标准差是1.0。
关于Math.random
Math.random()返回一个随机的double值,其本质其实是调用Random的nextDouble方法,源码如下:
1 public static double random() { 2 return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); 3 } 4 5 private static final class RandomNumberGeneratorHolder { 6 static final Random randomNumberGenerator = new Random(); 7 }
其内部的Random的实例是一个static,final修饰的,所以每一次都是利用的同一个Random实例获取的随机数。
最后,Random实现了Serializable接口,是可序列化的。并且自定义writeObject和readObject方法对象序列化进行了优化。
JDK8 新增的函数式编程方法
下列方法是JDK8新增的方法,用于流式编程:
1 IntStream ints(long streamSize)//产生streamSize个介于 2的负32次方~2的32次方之间的int型随机数 2 IntStream ints() //产生无穷多个介于 2的负32次方~2的32次方之间的int型随机数 3 IntStream ints(long streamSize, int randomNumberOrigin,int randomNumberBound)//产生streamSize个介于【randomNumberOrigin,randomNumberBound)之间的int型随机数 4 public IntStream ints(int randomNumberOrigin, int randomNumberBound)//产生无穷多个介于【randomNumberOrigin,randomNumberBound)之间的int型随机数 5 6 .......还有类似的long、double类型的系列方法
那么如何使用它们呢?请看下面的例子:
1 //产生10个[0,5)之间的随机数 2 new Random().ints(0,5).limit(10).forEach(System.out::println); 3 4 //list只有5个2的-32次方~2的32次方之间的int随机数 5 List<Integer> list = random.ints(5).limit(10).boxed().collect(Collectors.toList());
上面的例子中,因为Random相关的流式函数都是产生无穷多个随机数字流,所以只能使用惰性求值函数limit 进行截断,第一个例子产生10个【0,5)之间的随机数,当然有重复值是肯定的。第二例子中首先,boxed是所谓的自动装箱函数(即int封装成Integer),虽然limit截断10个,但是Random只产生了5个随机数,所以list中只有5个随机数。
另外,Java8并没有提供创建高斯(正态分布)伪随机数的流,但是利用现有的方法,也能够轻易的实现:
1 Stream.generate(random::nextGaussian).map(e -> e).limit(10).forEach(System.out::println);
generate方法是通过给定的 Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)产生一个无限长度的流,这里直接使用了Random的nextGaussian方法,用于产生无穷多个高斯随机数,接下来的map方法是一种类型转换方法,将一个流中的值转换成一个新的流。
关于函数式编程:
https://www.cnblogs.com/snowInPluto/p/5981400.html
https://www.cnblogs.com/aoeiuv/p/5911692.html
Random总结
首先,Random是线程安全的,它的内部维护了一个用于产生随机数的AtomicLong型种子变量seed,每一次产生随机数之前通过CAS的机制对种子变量进行更新,然后拿着更新过的种子计算出随机数。造成Random在高并发下使用同一个Random实例产生随机数的性能问题的根本,就在于多个线程需要以CAS的方式竞争该Random实例内部的那个共享的种子变量seed。明白了这一点,对于解决办法也就不难得出了,那就是每一个线程独立的维护一个用于自己产生随机数的种子变量(类似ThreadLocal的原理),这就不会存在竞争了,没错,下面要将的ThreadLocalRandom就是这样做的。
ThreadLocalRandom分析
Java7的实现
首先,对于ThreadLocalRandom的实现,JDK7和JDK8的实现方式是完全不一样的,不过它们的思想都是一样的,都借助了ThreadLocal的原理,不同的是JDK7直接使用了ThreadLocal来实现,而JDK8只是借助这种思想,并没有直接借助ThreadLocal来实现,下面是JDK7的部分实现源码:
1 public class ThreadLocalRandom extends Random { 2 3 private long rnd; 4 5 private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7; 6 7 ThreadLocalRandom() { 8 super(); 9 initialized = true; 10 } 11 12 public static ThreadLocalRandom current() { 13 return localRandom.get(); 14 } 15 16 private static final ThreadLocal<ThreadLocalRandom> localRandom = 17 new ThreadLocal<ThreadLocalRandom>() { 18 protected ThreadLocalRandom initialValue() { 19 return new ThreadLocalRandom(); 20 } 21 }; 22 23 public void setSeed(long seed) { 24 if (initialized) 25 throw new UnsupportedOperationException(); 26 rnd = (seed ^ multiplier) & mask; 27 } 28 protected int next(int bits) { 29 rnd = (rnd * multiplier + addend) & mask; 30 return (int) (rnd >>> (48-bits)); 31 } 32 33 ..... 34 }
以上只列举了JDK7中对ThreadLocalRandom实现的部分关键源码,从以上的源码可以看出以下几点,
- ThreadLocalRandom继承了Random;
- ThreadLocalRandom重写了setSeed和next(int bits)方法;
- 在内部维护了一个static的ThreadLocal<ThreadLocalRandom>类型的变量localRandom用来保存各个线程自己的ThreadLocalRandom实例;
- 增加了8个缓存行填充字段解决“伪共享”的问题,使每个线程的ThreadLocalRandom实例位于不同的缓存行,避免了竞争。
在JDK7的实现下,实际上是每个线程对应一个自己的ThreadLocalRandom实例,每一个实例独立的维护了各自的种子rnd,它通过Random的无参构造方法回调被重写的setSeed方法来初始化各自的种子变量rnd初始值。而被重写的next(int bits)方法则同Random的结构类似,是真正用于产生随机数其他方法的核心方法。
Java7中ThreadLocalRandom对各个线程的种子的初始化是基于父类Random的的逻辑和被重写的setSeed方法逻辑确定的,而父类Random的逻辑seedUniquifier()方法是通过AtomicLong +CAS机制实现的,所以这也保证了多个线程将获取到不同的初始种子,但前提是不要在多线程之间共享同一个ThreadLocalRandom实例。ThreadLocalRandom也是线程安全的。
Java8的实现
到了JDK8,ThreadLocalRandom的实现方法发生了变化,但是中心思想没有变,它不再使用ThreadLocalRandom实例本身维护线程各自的种子变量,而是将种子变量转移到Thread类中,如下所示,在JDK8的Thread类中新增了三个更ThreadLocalRandom有关的三个变量:
1 // The following three initially uninitialized fields are exclusively 2 // managed by class java.util.concurrent.ThreadLocalRandom. These 3 // fields are used to build the high-performance PRNGs in the 4 // concurrent code, and we can not risk accidental false sharing. 5 // Hence, the fields are isolated with @Contended. 6 7 /** The current seed for a ThreadLocalRandom */ 8 @sun.misc.Contended("tlr") 9 long threadLocalRandomSeed; 10 11 /** Probe hash value; nonzero if threadLocalRandomSeed initialized */ 12 @sun.misc.Contended("tlr") 13 int threadLocalRandomProbe; 14 15 /** Secondary seed isolated from public ThreadLocalRandom sequence */ 16 @sun.misc.Contended("tlr") 17 int threadLocalRandomSecondarySeed;
至于JDK的实现者为何要将种子变量放到Thread类中(并且利用了Contended注解解决了缓存行的伪共享问题),通过stackoverflow找到的答案是出于对时间(访问速度更快)和空间(占用内存更小)的更优考虑,就不再深究,下面就Java8中的实现进行分析:
1 public class ThreadLocalRandom extends Random { 2 3 private static final AtomicInteger probeGenerator = new AtomicInteger(); 4 private static final AtomicLong seeder = new AtomicLong(initialSeed()); 5 6 boolean initialized; 7 8 /** Constructor used only for static singleton */ 9 private ThreadLocalRandom() { 10 initialized = true; // false during super() call 11 } 12 13 static final ThreadLocalRandom instance = new ThreadLocalRandom(); 14 15 static final void localInit() { 16 int p = probeGenerator.addAndGet(PROBE_INCREMENT); 17 int probe = (p == 0) ? 1 : p; // skip 0 18 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); 19 Thread t = Thread.currentThread(); 20 UNSAFE.putLong(t, SEED, seed); 21 UNSAFE.putInt(t, PROBE, probe); 22 } 23 24 public static ThreadLocalRandom current() { 25 if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) 26 localInit(); 27 return instance; 28 } 29 30 public void setSeed(long seed) { 31 // only allow call from super() constructor 32 if (initialized) 33 throw new UnsupportedOperationException(); 34 } 35 36 final long nextSeed() { 37 Thread t; long r; // read and update per-thread seed 38 UNSAFE.putLong(t = Thread.currentThread(), SEED, 39 r = UNSAFE.getLong(t, SEED) + GAMMA); 40 return r; 41 } 42 43 protected int next(int bits) { 44 return (int)(mix64(nextSeed()) >>> (64 - bits)); 45 } 46 47 .... 48 }
- ThreadLocalRandom同样继承了Random类;
- ThreadLocalRandom同样重写了setSeed和next(int bits)方法,另外对产生随机数的方法nextInt、nextLong等方法也都进行了重新,因为他们的核心代码由nextSeed方法实现。
- ThreadLocalRandom的实例instance是static修饰的,所以对于多个线程,实例其实只有一个,而不像JDK7那样有多个。
ThreadLocalRandom的使用方式
使用ThreadLocalRandom的方式很简单,每次直接使用ThreadLocalRandom.current().nextInt()这种方式即可,特别需要注意的就是一定不要在多线程之间共享同一个ThreadLocalRandom实例,例如下面这种错误的用法:
1 public class RandomTest implements Runnable { 2 3 public static void main(String[] args) { 4 ExecutorService taskPool = Executors.newCachedThreadPool(); 5 for (int n = 1; n < 10; n++) { 6 taskPool.submit(new RandomTest()); 7 } 8 taskPool.shutdown(); 9 } 10 11 private static Random random = ThreadLocalRandom.current();//不要在多线程之间共享同一个实例 12 13 @Override 14 public void run() { 15 System.out.println(random.nextInt());//错误的使用方式 16 } 17 }
以上的使用方式是错误的,这种错误的使用方式在JDK7和JDK8下都将可能导致多个线程产生相同的随机数,为什么呢?从上面对源码的分析不难看出:
- 如果是JDK7,相当于各个线程都是共用存储与主线程的ThreadLocalMap中的同一个ThreadLocalRandom实例,而用于产生随机数的种子是存储在该ThreadLocalRandom实例中的,虽然每次产生随机数的时候都会更新种子rnd,但是JDK7的ThreadLocalRandom的种子不是原子变量,而是简单的long类型,且更新种子的方法没有加锁,因此将可能导致多个线程拿到相同的种子继而产生相同的随机数。
- 如果是JDK8,相当于各个线程共享的该ThreadLocalRandom实例的种子都基于主线程中的threadLocalRandomSeed变量,即各个线程使用了相同的初始种子,所以这10个线程当然就产生了相同的随机数序列,如果说JDK7是由于并发问题有可能导致某些线程产生相同的随机值,那么JDK8下将百分之百肯定所有线程产生的随机序列都将是相同的。
还有一点,那就是不论是JDK7还是JDK8,它们内部维护的种子变量都只是一个普通的int或者long型变量,因为已经不存在竞争了,所以也就没必要声明为原子变量了,当然也就不需要CAS的方式去改变它了,所以当多个线程使用相同的ThreadLocalRandom实例产生随机数的时候,产生相同的随机数也就说的通了。
所以正确的用法是,不要将ThreadLocalRandom.current()返回的实例在多个线程之间共享,而应该每一次需要产生随机数的时候都获取实例,同时立即调用产生随机数的方法,例如:
1 @Override 2 public void run() { 3 System.out.println(ThreadLocalRandom.current().nextInt());//每一次产生随机数时都要执行ThreadLocalRandom.current() 4 }
因为多线程情况下,ThreadLocalRandom.current()方法会帮我们找到各自线程内部维护的种子,从而产生各自的随机数,所以在每一次想要产生随机数的时候都要先执行ThreadLocalRandom.current()方法。
ThreadLocalRandom总结
不论是Random还是ThreadLocalRandom,它们产生随机数的总体方式都是一致的(不同的主要是对种子的产生算法与存储方式以及根据种子产生随机数的算法),那就是先通过一个通用的算法根据旧种子计算出新的种子替代旧种子,然后再根据新种子利用另一个通用算法产生随机数,可见产生随机数的算法是不变的,关键在于种子的不同,种子相同的时候在相同的运行环境下(JDK版本)产生的随机数肯定就是相同的,整个过程可以抽象为一个数学表达式:r = f(seed),seed是种子,r是产生的随机数,f就是那个通用算法,当然我这里是站在一个更高的维度进行的抽象,其实针对新种子的产生和最终随机数的产生是两个不同的通用算法:seed新=f1(seed旧),r = f2(seed新),f1,f2就代表那两个不同的通用算法。
本节首先讲解了Random的实现原理以及介绍了Random在多线程下存在竞争种子原子变量更新操作失败后自旋等待的缺点,从而引出ThreadLocalRandom类,ThreadLocalRandom使用ThreadLocal的思想,让每个线程内持有一个本地的种子变量(JDK7的种子放在ThreadLocalRandom实例中,JDK8的种子存放在Thread实例中),JDK8的种子变量只有在使用随机数时候才会被初始化,ThreadLocalRandom在多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,然后产生随机数,从而避免了竞争。
另外,除了Random和ThreadLocalRandom这种产生伪随机数的类,JDK还提供了产生安全的不可预测的随机数的类SecureRandom,这里由于篇幅限制就不再对其展开分析了,到用到的时候再看咯。

浙公网安备 33010602011771号