Java并发编程之美读书笔记5-Java并发包中ThreadLocalRandom类原理剖析

Posted 晓锋残月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程之美读书笔记5-Java并发包中ThreadLocalRandom类原理剖析相关的知识,希望对你有一定的参考价值。

文章目录

1. Random类

Random类常常用来获取某个随机数的生成,如下:

//1.获取一个int范围内的随机数
new Random().nextInt();
//2.获取一个指定范围为[0,5)之间的随机数
new Random().nextInt(5);

但是看起来没有任何问题的同时,没想到却存在线程安全问题。



2. Random类源码分析

1.首先创建一个下面测试方法:

public class RandomTest 

    public static void main(String[] args) 
        //(1)获取一个随机生成数
        Random random = new Random();

        //(2)输出10个0-5之间的随机数
        for (int i=0; i<10; i++) 
            System.out.println(random.nextInt(5));
        
    

上面是一个简单的生成随机数的方法,分为两部分查看:构造函数部分、生成随机数部分。


2.1 构造函数部分

设置一个断点,进入构造函数中:

在分析无参的Random()方法时,首先对于参数seedUniquifier

//创建了AtomicLong类型的变量,与Long类型相比,多了一个保证并发的原子性操作
private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);

由于其中还调用了seedUniquifier()方法和nanoTime()System.nanoTime()其实就是用来获取当前时间(但是不是以毫秒为单位,而是以纳秒为单位),具体可参考资料

private static long seedUniquifier() 
    for (;;) 
        // 获取上面定义的那个原子性long变量
        long current = seedUniquifier.get();
        // 计算next
        long next = current * 181783497276652981L;
        // 进行一个CAS操作,设置seedUniquifier的值为next
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    

执行完无参构造的函数后,该无参函数调用了有参构造函数。

initialScramble()方法中使用了一个产生随机数常用的线性同余发生器。有兴趣可以参考这里。通过构造函数基本上就产生了一个种子(这里通常称为old seed)。

通过上面的内容,就完成了Random对象的创建工作。


2.2 随机生成数部分

在主程序中调用了random.nexInt(5),不妨进入该方法中。

上面方法中,int r = next(31)用于获取一个31为int,首先不分析它的作用。然后在后面的工作就是一些求余数的工作,反正保证生成的随机数需要保证在[0,5)之间,具体细节可以参考。接下来分析next(31)是什么?

上面代码中,首先获取生成random对象时的old seed。然后通过线性同于生成器产生一个next seed。然后又是一个CAS操作(如果之前保存的seed(也就是oldseed)和现在读取到的oldseed是相同的,那么用nextseed更新当前oldseed),然后根据我们需要的bit位数返回。

问题:

(1)为什么传入的是int(31),而不是int(32)?

首先获取 31 位的随机数,注意这里是 31 位,和上面 32 位不同,因为在 nextInt() 方法中可以获取到随机数可能是负数,而 nextInt(int bound) 规定只能获取到 [0,bound) 之前的随机数,也就意味着必须是正数,预留一位符号位,所以只获取了31位。(不要想着使用取绝对值这样操作,会导致性能下降)

(2)上面方式如何产生的随机数?

我觉得这里的核心关键就是CAS操作,首先获取到oldseed,然后利用线性同余生成一个nextseed,当发现oldseed和Random类中保存的seed一致时,用nextseed去更新该seed。所以之后每次得到的oldseed和上一次不一样,从而导致后面的nextseed不一样。因而就是随机。


2.3 Random类问题分析

在单线程下每次在调用nextInt时都是根据老种子生成新种子,这可以保证生成随机数,但是在多线程下,多个线程可能拿到相同oldseed,然后在调用next(31)时得到的结果是一样的,显然这不是我们想要的,所以这也是为和在next(31)中需要使用CAS来保证原子性。然而,CAS 相比加锁有一定的优势,但并不一定意味着高效。同时只有一个线程会成功,其他线程需要自旋重试,这会降低并发性能,如果需要改进,那就是使用ThreadLocalRandom类。



3. ThreadLocalRandom类

(1)基本使用说明

首先注意其位置是java.util.concurrent包下

public class RandomTest 

    public static void main(String[] args) 
        //(1)获取一个随机生成数
        ThreadLocalRandom random = ThreadLocalRandom.current();

        //(2)输出10个0-5之间的随机数
        for (int i=0; i<10; i++) 
            System.out.println(random.nextInt(5));
        
    

(2)ThreadLocalRandom原理说明

其原理和ThreadLocal的原理一样,首先ThreadLocalRandom通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免 了对共享变量进行同步 。Random 的缺点是多个线程会使用同 一个原子性种子变量, 从而导致对原子变量更新的竞争。

如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能 。



4. ThreadLocalRandom源码分析

1.在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 
        //1.获取Unsafe实例
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        //2.获取Thread类里面threadLocalRandomSeed变量在Thread实例里面的偏移量
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        //3.获取Thread类里面threadLocalRandomProbe变量在Thread实例里面的偏移量
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        //4.获取Thread类里面threadLocalRandomSecondarySeed交量在Thread实例里面的偏移量,这个值在后面讲解LongAdder时会用到
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
     catch (Exception e) 
        throw new Error(e);
    

不知道对上面的三个值是否还有印象,这三个值是在Thread类中定义的。

public Thread
    /** 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; // 二级种子


4.1 构造函数部分

2.首先使用断点查看构造方法

在构造方法中根据PROBE来判断是否需要初始化。

// 3.方法是静态的,所以多个线程返回的是同一个ThreadLocalrandom实例
public static ThreadLocalRandom current() 
    //1.如果当前线程中的threadLocalRandomProbe的变量值为0(默认情况下,线程的这个值为0)
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        // 2.如果时,则说明当前线程是第一次调用ThreadLocalRandom的current方法,就需要调用localInit方法来计算当前线程的初始化种子变量
        localInit();
    return instance;

注意:这里通过使用这个if语句判断,当不需要随机数功能时就不初始化Thread类中的种子变量。这是一种优化。

其中对于localInit()方法

static final void localInit() 
    // 1.首先根据 probeGenerator 计算当前线程中 threadLocalRandomProbe 的初始化值
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    // 2.得到probe
    int probe = (p == 0) ? 1 : p; // skip 0
    // 3.计算得到seed种子
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    // 4。在当前线程中保存probe和seed
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);

通过上面步骤就完成了对象threadLocalRandom的创建。


4.2 随机数的生成

首先进入nextInt(int bound)方法中,发现其中调用了mix32nextSeed方法

nextSeed方法中有:

final long nextSeed() 
    Thread t; long r; // read and update per-thread seed
    // 首先获取threadLocalrandomSeed变量值为r,然后在这个基础上增加GAMMA作为新种子,然后使用putLong方法加入到当前线程的threadLocalRandomSeed变量中。
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;

可以发现,由于使用每个线程都各自维护了seed,所以不需要CAS操作,通过利用线程之间的隔离,减小并发冲突。所以整体上性能强于Random类。


4.3 注意事项

由于前面的构造函数可知,前面在构造方法处就已经生成了seed.所以如果对于下面的这种情况呢?

public class RandomTest2 

    public static void main(String[] args) 
        //(1)获取一个随机生成数
        ThreadLocalRandom random = ThreadLocalRandom.current();

        //(2)输出10个0-5之间的随机数
        for (int i=0; i<10; i++) 
            new Thread(new Runnable() 
                @Override
                public void run() 
                    System.out.println(random.nextInt(5));
                
            ).start();
        
    

上面的代码将会生成10个线程,知识不同时执行。但是他们都共享同一个random,所以得到的结果将是同一结果。

【重点】

所以建议使用ThreadLocalRandom.current().next(5)来生成某个随机数。



5. 性能测试

为了更加方便比较RandomThreadLocalRandom在多线程下性能比较,这里使用了JMH。【注意】:在使用之前,先学习一下JMH是什么

首先由如下的代码:

// 预热和执行分别设置3次预热执行,每个1秒进行实际操作,单位为微秒
@Warmup(iterations = 4, time = 1, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 4, time =1, timeUnit = TimeUnit.MICROSECONDS)
@Threads(10) // 设置进程数为10
@Fork(1) // 只需要执行一遍
@State(Scope.Benchmark)  // 工作线程共享变量内容
public class RandomBenchmark 

    Random random = new Random();

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public int random() 
        return random.nextInt();
    

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public int threadLocalRandom() 
        return ThreadLocalRandom.current().nextInt();
    

    public static void main(String[] args) throws RunnerException 
        Options opt = new OptionsBuilder()
                .include(RandomBenchmark.class.getSimpleName())
                .result("E:/randombenchmark.json")
                .resultFormat(ResultFormatType.JSON)
                .build();

        new Runner(opt).run();
    
    

执行代码:

Benchmark                          Mode  Cnt  Score   Error  Units
RandomBenchmark.random             avgt    4  1.153 ± 4.152  us/op
RandomBenchmark.threadLocalRandom  avgt    4  0.116 ± 0.211  us/op

其中图形结果为:

可以发现,使用ThreadLocalRandom果然可以明显增强性能。



6. 小结

以上便是对RandomThreadLocalRandom类的分析,其中以上知识点的学习参考了如下的内容,他们对于每一个知识点讲解同样细致,可以参考学习。

参考资料:

以上是关于Java并发编程之美读书笔记5-Java并发包中ThreadLocalRandom类原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发编程实践基础 读书笔记: 第三章 使用 JDK 并发包构建程序

java并发编程之美-阅读记录5

并发包中ScheduledThreadPoolExecutor

Java并发编程的艺术读书笔记——Java并发编程基础

编程之美读书笔记1.8 - 小飞的电梯调度算法

《java并发编程实战》读书笔记6--取消与关闭