Random和ThreadLocalRandom的实现原理
从JDK 7 开始引进了一个新的伪随机数生成器,ThreadLocalRandom,从名称可看出是一个与线程相关的Random,和之前的Random进行对比,ThreadLocalRandom在性能上和多线程并发处理上做了一些改进。
1,sun.misc.Unsafe
由于在产生伪随机数过程中,Random和ThreadLocalRandom都使用到了一个特殊的包:sun.misc.Unsafe。先对此包作个简单介绍。
sun.misc.Unsafe在JDK源代码中,有多处出现。为了更高效和更简单,它提供了一些与平台相关的更底层的功能,即提供了JNI的某些简单功能的替代,Unsafe的大部分方法都是Native方法。从名称也可以看出,它提供的是从Java意义上来说是“不安全”的一些功能,因此不应该在Java核心类库之外使用。实际上,除了通过反射机制,我们也无法获取到Unsafe的实例,使用它的功能。
更具体的介绍,可参看:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe。
2,理解Random:
从Random的介绍中可看出,一个Random类的实例可用于生成一个伪随机数。生成伪随机数的过程,是指定一个seed,然后通过一个公式来生成一个新的seed,并且根据新的seed值得到伪随机数。而如果两个不同的Random实例,指定的seed相同,则生成的伪随机数也相同。
代码展示: seed相同,则生成的随机数相同;
public static void main(String[] args) { // TODO Auto-generated method stub int currentseed = 100; System.out.println("current seed:" + currentseed); Random r1 = new Random(currentseed); int value1 = r1.nextInt(); Random r2 = new Random(currentseed); int value2 = r2.nextInt(); System.out.println("Random Object: " + r1.hashCode() + " AND " + r2.hashCode()); System.out.println("get random int:" + value1 + "," + value2); }
运行结果如下:
current seed:100 Random Object: 1311053135 AND 118352462 get random int:-1193959466,-1193959466
通过运行结果可以看出,相同的seed,不同的Random对象,生成的随机数是一样的。
在多个线程使用多个Random实例时,如果指定的seed是相同的,则生成的伪随机数序列也是相同的。因此需要使用不同的seed来创建Random实例,比如使用Random默认的seed,或使用当前的时间System.currentTimeMillis();
代码展示:多线程中,seed相同,则生成的随机数相同;
public static void main(String[] args) { // TODO Auto-generated method stub int currentseed = 100; System.out.println("current seed:" + currentseed); RandomThread rThread1 = new RandomThread(currentseed); new Thread(rThread1).start(); RandomThread rThread2 = new RandomThread(currentseed); new Thread(rThread2).start(); } public class RandomThread implements Runnable { private int seed; public RandomThread(int seed) { super(); this.seed = seed; } public void run() { // TODO Auto-generated method stub Random r1 = new Random(seed); try { for(int k =0 ;k<5;k++){ int value1 = r1.nextInt(); System.out.println("get random int" + k + ":" + value1); Thread.sleep(200); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
运行结果:
current seed:100 get random int0:-1193959466 get random int0:-1193959466 get random int1:-1139614796 get random int1:-1139614796 get random int2:837415749 get random int2:837415749 get random int3:-1220615319 get random int3:-1220615319 get random int4:-1429538713 get random int4:-1429538713
在两个线程中,实例化两个Random,使用相同的seed,则产生了完全相同的两个随机数序列。
从Random中获取随机数的源代码:
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)); } private static final long multiplier = 0x5DEECE66DL; private static final long addend = 0xBL; private static final long mask = (1L << 48) - 1;
,从源代码中可以看出,在生成随机数时,使用的公式:
nextseed = (oldseed * multiplier + addend) & mask;
其中,oldseed则为传进去的初始seed。而其他三个参数,multiplier,addend和mask,都为类静态变量。因此即使是不同的Random实例,生成的nextseed值也是相同的,所得的的随机数也就是相同的。
因此如果要确保随机数的效果,可以在多个线程中,使用同一个Random实例,这样就只需要维护同一套seed值。而Random实例生成随机数时对seed值的更新是一个原子操作,因此多个线程可能会在此产生阻塞而影响并发性能。
获取随机数时的最后一行操作,更新seed值,是一个原子操作,AtomicLong类中直接通过调用unsafe.compareAndSwapLong方法完成。
private final AtomicLong seed; //seed值设置成了final,当初始化seed值之后,再不能通过Java代码来修改seed值。 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)); } //seed.compareAndSet(oldseed, nextseed) 操作,调用unsafe的方法。
public final boolean compareAndSet(long expect, long update) { return unsafe.compareAndSwapLong(this, valueOffset, expect, update); }
其中 seed.compareAndSet(oldseed, nextseed),调用unsafe的方法来完成。该方法通过获得seed的内存位置偏移量,来设置seed的值,用nextseed值替换oldseed值。注意seed变量是设置成final的,因此无法通过Java程序再修改seed值,在此通过unsafe调用native方法来完成seed值的更新。
代码展示多个线程共享同一个random实例时,从程序结果看,有一部分获取随机数的操作,花费的时间比较多,说明多线程使用同一Random时,容易产生阻塞:
public static void main(String[] args) { // TODO Auto-generated method stub int currentseed = 100; Random currRandom = new Random(currentseed); for(int k=0;k<20;k++){ RandomThread rThread1 = new RandomThread(currRandom); new Thread(rThread1).start(); } } public class RandomThread implements Runnable { private Random currentRandom; public RandomThread(Random random) { super(); this.currentRandom = random; } public void run() { // TODO Auto-generated method stub try { for(int k =0 ;k<10;k++){ long currenttime = System.currentTimeMillis(); int value1 = currentRandom.nextInt(50); System.out.println("get random int" + k + ":" + value1); System.out.println("Cost time for random value:" + (System.currentTimeMillis() - currenttime)); Thread.sleep(100 * value1); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
从以上的分析,可总结出Random存在的几个问题:
生成多个实例,浪费资源;
相同的seed值,产生的伪随机数序列相同,效果不理想;
多个线程共享同一个实例时,会降低并发性能。
因此ThreadLocalRandom应运而生。
3, 理解ThreadLocalRandom:
针对Random存在的一些问题,ThreadLocalRandom有针对性的进行了改进。
ThreadLocalRandom是单例模式,系统只保存一个ThreadLocalRandom实例。
程序通过ThreadLocalRandom.current(); 获取ThreadLocalRandom实例。
public static ThreadLocalRandom current() { if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; } /** * Initialize Thread fields for the current thread. Called only * when Thread.threadLocalRandomProbe is zero, indicating that a * thread local seed value needs to be generated. Note that even * though the initialization is purely thread-local, we need to * rely on (static) atomic generators to initialize the values. */ 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); }
获取实例时,针对每个线程,只在第一次进行本地变量的参数初始化,多个线程的seed值也都是不同的。因此多个线程使用ThreadLocalRandom,获得的随机数也不一样。
但是ThreadLocalRandom针对每一个线程,都保存一份变量值在本地内存,在第一次使用时,初始化各个变量值,并且赋值给每个变量。
在更新seed值时,也都是在操作本地内存,避免了多线程操作时,对变量值的同步,大大提高了并发性能。
我们可以看到,在ThreadLocalRandom中定义了一些变量,而这些变量的值,是通过Thread线程保存在本地,即每个线程都保存了一份变量:
//ThreadLocalRandom中各变量的定义,以及通过Unsafe,获取保存在本地内存中的地址。 // 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); } } //Thread类中对每个变量的定义: // The following three initially uninitialized fields are exclusively // managed by class java.util.concurrent.ThreadLocalRandom. These // fields are used to build the high-performance PRNGs in the // concurrent code, and we can not risk accidental false sharing. // Hence, the fields are isolated with @Contended. /** The current seed for a ThreadLocalRandom */ @sun.misc.Contended("tlr") long threadLocalRandomSeed; /** Probe hash value; nonzero if threadLocalRandomSeed initialized */ @sun.misc.Contended("tlr") int threadLocalRandomProbe; /** Secondary seed isolated from public ThreadLocalRandom sequence */ @sun.misc.Contended("tlr") int threadLocalRandomSecondarySeed; //在ThreadLocalRandom中,是针对每个线程,进行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; }
在ThreadLocalRandom中,是针对每个线程,对该线程的变量seed进行读取和更新。不存在多个线程共享的问题,因此也不存在影响并发性能的问题。