使用当前时间的随机数生成器与不使用的区别

13

我想了解使用以System.currentTimeMillis()作为种子的随机数生成器和仅使用默认构造函数之间的区别。也就是说,这两者之间有什么区别:

Random rand = new Random(System.currentTimeMillis());

还有这个:

Random rand = new Random();
我知道这些数字是伪随机的,但是我还没有完全理解其细节以及它们的生成方式,包括在使用当前时间作为种子和使用默认构造函数时所得到的“随机性”水平之间的差别。

你能告诉我们在提出这个问题之前你查阅了哪些文章、指南或教程吗?这可能有助于我们更准确地理解你不理解的内容。 - CubeJockey
1
https://dev59.com/lHnZa4cB1Zd3GeqPoErj - arunmoezhi
3个回答

8

如果你想让随机序列在不同的运行中相同,可以指定一个种子。通常情况下,您不希望这种情况发生,因此每次运行时使用不同的种子是合理的。常用的种子是System.currentTimeMillis()

如果您正在编写一个多线程程序,其中多个线程将同时初始化Random对象,则可能需要避免使用System.currentTimeMillis(),而是让Java使用其自己的初始化。


5
一般而言,你总是希望让Java使用自己的初始化。这比System.currentTimeMillis()更好,所以你为什么要降级到它呢? - Andreas
@Andreas 谢谢!你的评论解决了我的困惑!我一直认为使用 System.currentTimeMillis() 有点多余。我是对的吗?事实上,在我的一个程序中,当我使用 Random 类时,我惊讶地发现当我使用 System.currentTimeMillis() 时性能更差。我无法找出原因。我基本上只是感到困惑,但我想,好吧,让我们使用那个有效的方法!顺便说一下,我不知道是否应该将此答案标记为正确,因为真正回答我的问题的是这个评论! - ayePete
@Andreas,如果您能根据上面的评论给出更详细的答案,那我将不胜感激,并会接受它。非常感谢! - ayePete
1
@ayePete System.currentTimeMillis() 明显是多余的。我不确定你所说的“性能下降”是什么意思。你是否正在初始化多个 RNG?你可能想使用单例模式或为每个线程保留一个 RNG。 - Josep Valls
@Josep Valls,我主要研究使用启发式方法解决组合优化问题,在我提到的情况下,使用默认构造函数得到的解更接近最优解,并且更频繁,而不是使用System.currentTimeMillis。据我回忆,我在代码的各个点初始化了多个RNG,因此我不知道这是否对生成的值产生了影响。您是否间接建议在整个程序中使用单个RNG?我只使用了一个线程。 - ayePete
1
@ayePete,即使在JDK 6中,更简单的实现也使用了System.nanoTime()和一个不断变化的种子来减少多线程冲突。System.currentTimeMillis()是内置实现的降级版本。故事的寓意是:不要指定种子,除非你想要在多次运行中重复使用种子。 - Andreas

8
提供自己的种子对于那些想要多次生成相同伪随机值序列的模拟非常有用。然而,通常情况下,使用默认构造函数也同样好。当使用默认构造函数时,文档中说:这个构造函数将随机数生成器的种子设置为一个非常可能与此构造函数的任何其他调用不同的值。换句话说,它在内部生成自己的种子。具体细节取决于所使用的特定Java实现。我见过的一种实现方式是这样的:
private static volatile long seedBase = 0;

public Random() {
    setSeed(System.nanoTime() + seedBase);
    ++seedBase;
}

实际的随机性质并不会改变。如果您关心随机序列的质量,还可以使用 java.security.SecureRandom,它具有更好的加密行为。(例如,请参见 this thread。)

我很好奇,你在哪里看到那个实现的?除了在易失字段上滥用 ++ 之外,它比 JDK 7 或 JDK 8 使用的实现要简单得多。 - yshavit
1
@yshavit - 这是Android API 22库中使用的一个。 - Ted Hopp
@yshavit JDK 6同样简单。 - Andreas
@yshavit - 我认为volatile字段上的竞态条件并不是一个严重的问题;即使两个线程互相覆盖,System.nanoTime() + seedBase在后续调用中相同的概率非常小,在任何情况下,合同只保证“很可能是不同的”。我同意你关于这是一个糟糕的实现的看法;使用AtomicLong会更好。 - Ted Hopp
@TedHopp 我完全同意,这不是一个严重的问题 - 尽管我对相似种子的可能性持有不同意见:如果两个线程竞争得足够接近以至于它们彼此覆盖,那么它们拥有相同的 nanoTime() 是非常可能的,特别是考虑到大多数平台实际上并没有提供纳秒级精度(我的 Mac 似乎“只”提供微秒级精度)。但是,像你一样,我认为 AtomicLong 应该是更好的选择 - 至少可以建立良好的习惯! - yshavit

2
如果您查看Random的默认构造函数的实现,您会发现它在内部使用System.nanoTime()。另外,它使用一个种子"唯一化器"来使后续的种子更加不同。然而,这需要访问一个static final AtomicLong。因此,如果您有一个高并发的应用程序,在其中许多线程正在构建Random实例,最好不要使用默认构造函数以避免争用种子生成。如果您想保证两个线程永远不会得到相同的种子,那么应该使用默认构造函数。
话虽如此,在实践中,在99%的情况下,选择哪种变体将是无关紧要的。
正如Ted Hopp正确地指出的那样,默认构造函数的行为取决于具体的JDK实现,并且在Java版本之间也会有所变化。
另请参见concurrent use of java.util.Random中的争用,了解Random类的另一个争用问题。

种子生成的内部细节因Java实现而异。在Java 7和8中,“唯一标识符”是一个AtomicLong,可以避免您所描述的多线程问题。 - Ted Hopp
这取决于使用情况。如果您想在任何情况下避免不同线程具有相同的种子,那么您是正确的。但是,如果您想避免争用并且可以接受相同的种子,则AtomicLong是一个问题。 - Jan Schaefer
无论AtomicLong是否是一个问题,我的观点只是,如果像你所说,“你查看Random默认构造函数的实现”,则会发现根据你使用的Java版本不同而有不同的事情。在某些实现中,“唯一标识符”只是一个volatile字段。虽然没有线程竞争,但存在竞争条件(也没有确保唯一性)。 - Ted Hopp
当然,你是正确的,因为有不同的实现。但如果你正在使用Java 7或8,这仍然是相关的。另请参阅https://dev59.com/AGEh5IYBdhLWcg3whT0S - Jan Schaefer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接