随机是日常生活中经常遇到的非常有趣的东西,比如说抛硬币,他的不可预知性总是让我们特别着迷,在拿不定主意时,有些人就喜欢用抛硬币的方式来帮助我们做决定。体育领域也喜欢用喜欢用抛硬币的方式来猜先。随机数功能是Java非常非常基础的功能。早在1.0的版本就引入了Random类。
Random
Random类的使用
public static void main(String[] args) {Random random = new Random();// 0-10的随机整数int randomResult = random.nextInt(10);System.out.println(randomResult);
}
Random的实现原理
Random通过一个种子(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));
}
其中multiplier,addend和mask是在类中写死的,这种算法叫线性同余算法(LCG)有兴趣的同学可以自己去了解一下,这里就不展开了。结论是算法会造成一种情况是相同的因子的相同生成次数会得到相同的数。
public static void main(String[] args) {Random random1 = new Random(1000L);Random random2 = new Random(1000L);for (int i = 0; i < 5; i++) {System.out.printf("%nrandom1的第%s次生成随机数" + random1.nextInt(100), i);System.out.printf("%nrandom2的第%s次生成随机数" + random2.nextInt(100), i);}
}
得到结果
由于Random使用LCG算法生成伪随机数,而且Random的随机数是可预测的会造成安全问题,所以我们需要一个安全的真随机数生成器:SecureRandom。
SecureRandom
内置两种随机数算法:SHA1PRNG和NativePRNG。也支持基于SPI机制对算法进行扩展。默认是NativePRNG算法。SecureRandom类会收集一些随机事件,比如鼠标点击、键盘敲击等,SecureRandom使用这些随机事件作为种子,使种子不再是可以预测的。
ScureRandom类的使用
public static void main(String[] args) throws NoSuchAlgorithmException {// 使用默认算法:NativePRNG// SecureRandom random = new SecureRandom();// 指定算法:SHA1PRNGSecureRandom random = SecureRandom.getInstance("SHA1PRNG");int result = random.nextInt(10);System.out.println(result);
}
Random的线程安全是通过AtomicLong的CAS操作保证的,java7版本发布了多线程情况下的性能优化版本的Random类:ThreadLocalRandom。
ThreadLocalRandom
ThreadLocalRandom有两项重要改动。
第一项改动是,弃用AtomicLong,将种子seed改为普通的Long类型,避免了高竞争的情况下CAS的性能下降问题;
第二项改动是,我们不需要再创建ThreadLocalRandom对象实例,构造函数已经设置成私有的了,可以通过ThreadLocalRandom.current()获取;
ThreadLocalRandom类的使用
public static void main(String[] args) {for (int i = 0; i < 10; i++) {int result = ThreadLocalRandom.current().nextInt(10);System.out.println(result);}
}
在学习时,也看了其他大佬的文章,看到了他们提到的错误用法,我在这里记录一下,避免你们实践的时候会遇到。
public class RandomTest {private static ThreadLocalRandom RANDOM = ThreadLocalRandom.current();public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Player().start();}}private static class Player extends Thread {@Overridepublic void run() {System.out.println(getName() + ": " + RANDOM.nextInt(100));}}
}
输出结果
这是为什么呢,实话实说我疑惑了一阵子,后来发现,所有的子线程都是在用main现场初始化的ThreadLocalRandom对象,没有经过初始化,子线程的SEED没有经过初始化就是0,大家的SEED都是相同的情况下,自然就会跟Random中落入相同的seed相同的次数获取的值都相同的陷阱中。