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
上述内容,如有错误,欢迎指正
作者:DFYoung
来源链接:https://blog.csdn.net/dh554112075/article/details/104069945
版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。
2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。