Java随机数Random解析

首先介绍一下随机数生成的概念

Java随机数的核心就是种子,在Random中其实就是一个AtomicLong类型的变量。

private final AtomicLong seed;

随机数的生成依赖于种子,当我们通过new Random()创建一个随机数生成器,会通过计算为我们默认生成一个种子的计算值,再通过这个种子的计算值运算得出最终的种子。下面这段代码是Random默认生成种子计算值的逻辑。

public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

private static long seedUniquifier() {
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

我们也可以在初始化时给定种子的计算值,再通过计算生成最终的种子。

public Random(long seed) {
	// 主要就是下面这行代码
    this.seed = new AtomicLong(initialScramble(seed));
}
// 通过计算让种子变得复杂
private static long initialScramble(long seed) {
    return (seed ^ multiplier) & mask;
}

111111111111111111111111111111111111111111111111——mask(48位二进制)
10111011110111011001110011001101101——multiplier

—— 总结:随机数的生成依靠种子,种子的生成依靠于种子的计算值,种子的计算值可以由我们给定,也可以由Random默认的通过当前时间的纳秒计算得到,种子的计算值通过一系列的运算得到最终的种子。


下面来介绍随机数是如何生成的

上面提到,随机数的生成依靠于种子,通过一个种子生成出随机数后,那么该种子就不可用了,因为通过该种子生成的随机数还会是一样的,因此需要通过旧的种子生成新的种子,以应对下一次随机数的生成。

下面先看看Random.next()方法,该方法用于生成新的种子。
nextInt(),nextLong()等方法都是通过调用该方法保留种子的固定长度,再通过该部分种子计算出随机数。
eg.int则保留31位,long则保留63位——

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)); // 新的种子保留指定位数的长度
}

下面只举nextInt一个例子,剩下的也只是计算随机数所使用的算法不同。

public int nextInt(int bound) {
	int r = next(31); // 保留31位的种子
	int m = bound - 1; // 生成随机数的最大值,也就是不包含bound边界值
	if ((bound & m) == 0) // 如果边界值是2的幂
		// eg. bound = 8, r = xxx,xxx是31位2进制)
		// 1111111111111111111111111111110 * 1000 =
		// 1111111111111111111111111111110000(相当于直接把000加在了原来的二进制后面)
		// 1111111111111111111111111111110000 >> 31 = 111
		r = (int)((bound * (long)r) >> 31);
	else {
		for (int u = r;
			 u - (r = u % bound) + m < 0;
			 u = next(31))
			;
	}
	return r;
}

—— 总结:随机数的生成依靠种子,种子的生成依靠于种子的计算值,种子的计算值可以由我们给定,也可以由Random默认的通过当前时间的纳秒计算得到,种子的计算值通过一系列的运算得到最终的种子。每次都需要通过新生成的种子来构建我们的随机数。


Random真的存在线程安全问题吗?

什么叫Random发生了线程安全问题?—— 当我们使用相同的种子去生成随机数,那么这两个随机数会是相同的,也就是如果我们使用多个线程去操作同一个Random,如果多个线程使用相同的种子去生成随机数,那么就会生成一系列相同的随机数。

但Random真的线程不安全吗?

回到之前这个通过旧种子生成新种子的逻辑,多个线程可以同时获取相同的旧种子,但这里通过CAS保证了只有一个线程可以通过旧种子生成新的种子 —— 也就是Random是线程安全的,但因为使用CAS,在高并发情况下,如果生成随机数频率过高,会影响效率。

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在高并发下效率低,因此考虑使用ThreadLocalRandom。线程会冲突的只有种子,因此只要把种子交给每个线程维护即可,因此结合ThreadLocal就成了ThreadLocalRandom,这里就不介绍了。


下面内容可以不看

通过代码进行测试,下面代码本来是用来测试Random是线程不安全的
(之前对Random印象不深,总是忘记Random在高并发下是效率低的,而不是线程不安全的)

1)创建自定义Random(copy代码),维护一个线程安全的Set(seedSet)用于存储新生成的种子

private Set<Long> seedSet = Collections.synchronizedSet(new HashSet<>());

2)修改next()方法,在成功生成新的种子后,将种子添加到seedSet

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));
    seedSet.add(nextseed);
    return (int)(nextseed >>> (48 - bits));
}

3)自定义Random暴露一个seedSize()方法,用于查看生成的种子数

public int seedSize() {
    return seedSet.size();
}

4)下面创建200个线程,每个线程生成1000个随机数存到自己的HashSet,最后对每个线程生成的HashSet进行汇总,最后打印结果。

public class Test {

    public static void main(String[] args) throws Exception {
        Random random = new Random();
        Set<Integer>[] sets = new Set[200];
        CountDownLatch countDownLatch = new CountDownLatch(200);
        for (int i = 0; i < 200; i++) {
            new Thread(new RandomThread(random, sets, i, countDownLatch)).start();
        }
        countDownLatch.await();
        Set<Integer> totalSet = new HashSet<>();
        for (int i = 0; i < 200; i++) {
            for (int j : sets[i]) {
                totalSet.add(j);
            }
        }
        System.out.println("预计" + (1000 * 200) + ",实际" + totalSet.size() + ",重复了" + ((1000 * 200) - totalSet.size()));
        System.out.println("总共生成的种子数:" + random.seedSize());
    }

}

class RandomThread implements Runnable {

    private Random random;
    private Set<Integer>[] sets;
    private int num;
    private CountDownLatch countDownLatch;

    public RandomThread(Random random, Set<Integer>[] sets, int num, CountDownLatch countDownLatch) {
        this.random = random;
        this.sets = sets;
        this.num = num;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Set<Integer> set = sets[num];
            if (set == null) {
                set = new HashSet<>(1000);
                sets[num] = set;
            }
            set.add(random.nextInt(Integer.MAX_VALUE));
        }
        countDownLatch.countDown();
    }
}

测试多次结果

预计200000,实际199983,重复了17
总共生成的种子数:200000
预计200000,实际199991,重复了9
总共生成的种子数:200000
预计200000,实际199993,重复了7
总共生成的种子数:200000

上述内容,如有错误,欢迎指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值