尽可能地,RNG的种子应该本身就是随机的。你正在使用的种子只会在一两位上有所不同。
很少有好的理由在一个程序中创建两个独立的RNG。你的代码并不是其中有意义的情况之一。
只需创建一个随机数生成器并重复使用,就不会出现这个问题。
回复 mmyers 的评论:
Do you happen to know java.util.Random
well enough to explain why it picks 5
and 6 in this case?
答案在java.util.Random的源代码中,它是一个线性同余随机数生成器。当你在构造函数中指定一个种子时,它会按以下方式操作。
seed = (seed ^ 0x5DEECE66DL) & mask;
口罩只保留低48位,不保留其他位。
在生成实际随机位时,使用以下方法操纵种子:
randomBits = (seed * 0x5DEECE66DL + 0xBL) & mask;
现在如果考虑到 Parker 使用的种子是连续的(0-1499),并且只使用一次就被丢弃,那么生成的前四个种子产生了以下四组随机位:
101110110010000010110100011000000000101001110100
101110110001101011010101011100110010010000000111
101110110010110001110010001110011101011101001110
101110110010011010010011010011001111000011100001
请注意,每种情况下的前10位是相同的。这是一个问题,因为他只想要生成0-7范围内的值(只需要几个比特),RNG实现会将高位向右移位并丢弃低位以实现此目的。它这样做是因为在一般情况下,高位比低位更随机。在这种情况下,它们并不是因为种子数据很差。
最后,要了解这些二进制位如何转换为我们得到的十进制值,您需要知道当n是2的幂时,java.util.Random会做出特殊情况。它请求31个随机位(上述48个位中的前31个),将该值乘以n,然后将其右移31位。
乘以8(在这个例子中的n值)就相当于将左移3位。因此,这个过程的净效应是将31位向右移动28个位置。在上面4个例子中的每一个中,这将留下位模式101(或十进制中的5)。
如果我们不仅仅在一个值后抛弃RNGs,我们会看到序列分散。虽然以上四个序列都是以5开头,但每个序列的第二个值分别是6、0、2和4。种子的微小差异开始产生影响。
作为对更新问题的回答:java.util.Random是线程安全的,您可以在多个线程之间共享一个实例,因此仍然不需要具有多个实例。如果您确实需要多个RNG实例,请确保它们彼此完全独立地进行种子处理,否则您无法相信输出是独立的。
关于为什么会出现这种效果,java.util.Random并不是最好的随机数生成器。它很简单,速度相当快,如果你不仔细查看的话,似乎是随机的。然而,如果你对它的输出进行一些严格的测试,你会发现它有缺陷。你可以在这里通过视觉方法看到这些缺陷。
如果您需要更加随机的RNG,您可以使用java.security.SecureRandom。它的速度稍微慢一些,但它可以正常工作。但是可能会有一个问题,它是不可重复的。使用相同的种子生成的两个SecureRandom实例将不会得到相同的输出结果。这是有意设计的。
还有哪些其他选择?这就是我推荐我的自己的库的地方。它包括3个可重复的伪随机数生成器,比SecureRandom更快,比java.util.Random更随机。我没有发明它们,我只是从原始的C版本移植了它们。它们都是线程安全的。
我实现了这些RNG,因为我需要一个更好的进化计算代码。根据我的最初简短回答,这个代码是多线程的,但它只使用一个RNG实例,共享给所有线程。