Random在多线程下的问题以及ThreadLocalRandom类分析

Ramdom在多线程环境下的问题

首先我们看一下Random类的nextInt源码:

//产生[0,bound)的随机数
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //生成一个31位的随机数
    int r = next(31);
    int m = bound - 1;
    // 如果bound是2的整数幂次方
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    // 通过除余运算产生一个小于bound的值
    else {
        for (int u = r;
                u - (r = u % bound) + m < 0;
                u = next(31))
            ;
    }
    return r;
}

可以看出,产生随机数的核心逻辑在于next方法中,并且实际上Random类的其他常用方法都是基于next方法。

那么next方法是如何实现的:

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));
}

可以看出,Random类的内部维护了一个随机数种子,并且类型为AtomicLong,那为什么要用AtomicLong呢?

可以这样想:在多线程环境下,多个线程同时调用next方法,同时获取旧种子并使用固定的算法生成新种子。那么每个线程产生的新种子的值自然是相同的,产生的随机数也是相同的,这当然是不希望看到的。

于是,Random类将种子定义为AtomicLong类型,并通过CAS自旋锁的方式去更改随机数种子的值,若多个线程同时获取旧种子的值,并产生相同的新种子,但只要有一个线程通过CAS设置了新种子的值,后面的线程就无法通过CAS进行更改,必须重新获取种子的值并生成新种子。

通过CAS自旋之后,多线程下的问题是解决了,但是却产生了效率问题:多个线程争抢一个原子变量,只有一个线程会成功,而其他线程会在CAS上自旋重试,影响了并发性能。

ThreadLocalRandom分析

ThreadLocalRandom是JDK1.7为了解决Random并发效率问题产生的类,先来看看ThreadLocalRandom怎么用:

public static void main(String[] args) {
    Runnable runnable = ()->{
        // 通过静态方法获取ThreadLocalRandom对象
        ThreadLocalRandom random = ThreadLocalRandom.current();

        System.out.println(random.nextInt(100));
    };

    for(int i = 0;i<10;i++){
        new Thread(runnable).start();
    }
}

ThreadLocalRandom.current()获取的到底是不是同一个对象呢?看看源码:

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

PROBE是什么东西呢?可以在ThreadLocalRandom中找到如下静态字段和静态代码块:

// 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);
    }
}

UNSAFE.objectFieldOffset这个方法就厉害了,是获取指定的变量在所属类中的内存偏移地址,查看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;

其中threadLocalRandomSeed是线程持有的随机数种子,threadLocalRandomProbe可以范围为探针,大概是用来辅助的,threadLocalRandomSecondarySeed是备用的随机数种子。

@sun.misc.Contended注解是避免伪共享问题进行字段填充。

于是回到current方法:

public static ThreadLocalRandom current() {
    //getInt是通过偏移地址获取对象中的字段值
    //如果为0说明还没初始化过,进行初始化
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

这里注意到其实return的对象是一个内部的静态对象,是单例的:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

那么localInit方法初始化的是什么呢?联系上面看到的Thread中的字段可以猜想也许是初始化线程中自己持有的Seed、Probe等:

static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    //probe不能等于0
    int probe = (p == 0) ? 1 : p; // skip 0
    //产生随机数种子
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    // 将probe和seed设置到当前线程的字段中
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

其中probeGeneratorseeder都是一个AtomicInteger类型,通过addAndGet一个固定的偏移量为每个线程产生不同的字段取值(seed还要再经过混淆)。

可以看到,实际上,ThreadLocalRandom是让各个线程持有了自己的随机数种子,这点和ThreadLocal很像。

然后再看一下nextInt方法:

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //产生新的种子后用种子做混淆
    int r = mix32(nextSeed());
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
                u + m - (r = u % bound) < 0;
                u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

通过nextSeed产生新的种子:

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基本一样。通过以上分析可以看出,线程通过ThreadLocalRandom.current()获取的是同一个ThreadLocalRandom对象,但是因为各个线程内部维护的随机数种子不一样(第一次调用current会触发初始化),所以产生的随机数也会不一样。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/random%e5%9c%a8%e5%a4%9a%e7%ba%bf%e7%a8%8b%e4%b8%8b%e7%9a%84%e9%97%ae%e9%a2%98%e4%bb%a5%e5%8f%8athreadlocalrandom%e7%b1%bb%e5%88%86%e6%9e%90/

(0)
彭晨涛彭晨涛管理者
上一篇 2020年5月18日
下一篇 2020年5月19日

相关推荐

  • Java基础查缺补漏05

    继续我的复习刷题 可以有和类名同名的函数 题目: JAVA中,下列语句哪一个正确() A. class中的constructor不可省略B. constructor必须与class…

    Java 2020年5月29日
    0140
  • Java字节码实例探究

    深入理解java虚拟机第三版读书笔记06中介绍了class文件结构,这里我们动手实践,编译一个类查看一下它的字节码。 java源码: public class Main { pri…

    2020年1月23日
    0220
  • AbstractCollection默认集合类

    AbstractCollection用于实现基本的Collection结构,提供给普通用户继承使用。也是JDK集合类的父类,部分方法是没有被重载的。 相比Collection接口并…

    Java 2019年11月18日
    0100
  • 基于BIO模型实现多人聊天室

    第一版 服务端实现: public class ChatServer { private int DEFAULT_PORT = 8888; private final String…

    Java 2020年2月6日
    0380
  • Java中的四种内部类

    我发现最近真是越来越没有东西写了。。。不可能天天学习新知识啊,最近在复习阶段了,复习的东西大多数是博客里写过的/(ㄒoㄒ)/ 复习Java基础的时候认真看了一下Java的内部类,这…

    Java 2020年5月23日
    0100
  • 按键排序的Map-TreeMap源码分析

    总结 总结放前面(这篇挺短的)1. TreeMap基于红黑树实现,可以对Map中的key排序2. 它的排序和定位需要依赖比较器或实现 Comparable 接口,也因此不需要key…

    Java 2020年2月16日
    0130
  • ArrayList源码分析

    总结 总结放前面防止太长不看: ArrayList内部是用数组实现的。 如果使用无参构造函数建立ArrayList,在添加第一个元素的时候会分配10个元素的空间。 ArrayLis…

    2019年11月22日
    0140
  • 深入理解java虚拟机第三版读书笔记12

    以下是第十二章 Java内存模型与线程的内容 硬件的效率与一致性 基于高速缓存的存储交互很好地解决了处理器与内存速度之间的矛盾,但是也为计算机系统带来更高的复杂度,它引入了一个新的…

    2020年1月29日
    01530
  • JUC包下的信号量Semaphore

    概述 信号量,用来限制能同时访问共享资源的线程上限。 public static void main(String[] args) { // 1. 创建 semaphore 对象 …

    Java 2020年2月6日
    0180
  • JDK8新增高效原子累加器LongAdder源码分析

    很久以前写过CAS应用之JUC下的原子类,但是LongAdder这个类没有去看,只是给了一个其他博客的参考链接。今天就自己来分析一下。 AtomicLong的问题和LongAdde…

    2020年5月19日
    0710

发表回复

登录后才能评论