播种连续编号的java.util.Random

Seeding java.util.Random with consecutive numbers

我将我遇到的一个bug简化为以下几行代码:

1
2
3
4
    int[] vals = new int[8];
    for (int i = 0; i < 1500; i++)
        vals[new Random(i).nextInt(8)]++;
    System.out.println(Arrays.toString(vals));

输出为:[0,0,0,0,0,1310,190,0]

这只是一个选择连续数字随机种子然后使用2次幂的nextint的人工制品吗?如果是这样的话,是否还有其他我应该意识到的陷阱?如果不是,我做错了什么?(我不是在寻找解决上述问题的方法,只是想了解一下还有什么可能出错)

丹,写得很好的分析。由于JavaDoc对数字的计算方式非常明确,所以这并不是一个谜,为什么会发生这样的情况,就像有其他异常情况需要注意一样——我没有看到任何关于连续种子的文档,我希望有java.util.random经验的人能够指出其他常见的陷阱。

对于代码,需要几个并行代理具有可重复的随机行为,这些行为恰好从枚举8元素中选择,只要它们的第一步。一旦我发现了这种行为,种子都来自于从已知种子创建的主随机对象。在前一个(顺序种子)版本的程序中,在第一次调用nextint之后,所有的行为都很快发生了差异,所以我花了相当长的时间将程序的行为缩小到RNG库中,我希望将来避免这种情况。


尽可能地,RNG的种子本身应该是随机的。你使用的种子只会有一两个比特的差别。

很少有好的理由在一个程序中创建两个单独的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的源代码中,它是一个线性同余RNG。当您在构造函数中指定种子时,它将按如下操作。

1
seed = (seed ^ 0x5DEECE66DL) & mask;

其中,掩码只保留较低的48位并丢弃其他位。

在生成实际随机位时,此种子将按以下方式操作:

1
randomBits = (seed * 0x5DEECE66DL + 0xBL) & mask;

现在,如果您认为Parker使用的种子是连续的(0-1499),并且它们曾经被使用过,然后被丢弃,那么前四个种子会生成以下四组随机位:

1
2
3
4
101110110010000010110100011000000000101001110100
101110110001101011010101011100110010010000000111
101110110010110001110010001110011101011101001110
101110110010011010010011010011001111000011100001

注意,前10位在每种情况下都是缩进的。这是一个问题,因为他只想生成0-7范围内的值(只需要几个位),而RNG实现通过将较高的位右移并丢弃较低的位来实现这一点。这是因为在一般情况下,高位比低位更随机。在这种情况下,它们不是因为种子数据很差。

最后,要了解这些位如何转换为我们得到的十进制值,您需要知道java.util.random在n是2的幂时会出现特殊情况。它请求31个随机位(上面48个的前31位),将该值乘以n,然后将其右移31位。

乘以8(本例中n的值)与向左移动3个位置相同。所以这个过程的净效果是将31位28移到右边。在上面的4个例子中,每一个都会留下位模式101(或十进制的5)。

如果我们不在一个值后丢弃RNG,我们会看到序列发散。当上述四个序列都以5开头时,每个序列的第二个值分别为6、0、2和4。最初种子的微小差异开始产生影响。

对于更新后的问题:java.util.random是线程安全的,您可以跨多个线程共享一个实例,因此仍然不需要有多个实例。如果您真的需要有多个RNG实例,请确保它们是完全独立地播种的,否则您不能相信输出是独立的。

至于为什么会产生这种效果,java.util.random并不是最好的RNG。它很简单,很快,如果你看得不太近,也相当随意。但是,如果您对它的输出运行一些严格的测试,您将看到它有缺陷。你可以在这里直观地看到。

如果需要更随机的RNG,可以使用java.security.securelrandom。它有点慢,但工作正常。但有一件事可能对你来说是个问题,那就是它是不可重复的。具有相同种子的两个SecureRandom实例不会给出相同的输出。这是按设计的。

那么还有什么其他的选择呢?这是我插入自己的库的地方。它包括3个可重复的伪RNG,它们比SecureRandom更快,比java.util.Random更随机。我没有发明它们,只是从原始的C版本移植了它们。它们都是线程安全的。

我实现这些RNG是因为我需要更好的东西来编写进化计算代码。根据我最初的简短回答,这段代码是多线程的,但它只使用一个RNG实例,在所有线程之间共享。