【问题标题】:Why is ThreadLocalRandom implemented so bizarrely?为什么 ThreadLocalRandom 的实现如此离奇?
【发布时间】:2017-03-29 22:29:00
【问题描述】:

这个问题是关于ThreadLocalRandom在OpenJDK 1.8.0版本中的实现。

ThreadLocalRandom 提供了一个每个线程的随机数生成器,没有 Random 带来的同步开销。最明显的实现 (IMO) 是这样的,它似乎保留了向后兼容性而没有太多复杂性:

public class ThreadLocalRandom extends Random {
    private static final ThreadLocal<ThreadLocalRandom> tl =
        ThreadLocal.withInitial(ThreadLocalRandom::new);
    public static ThreadLocalRandom current() {
        return tl.get();
    }
    // Random methods moved here without synchronization
    // stream methods here
}

public class Random {
    private ThreadLocalRandom delegate = new ThreadLocalRandom();
    // methods synchronize and delegate for backward compatibility
}

然而,实际的实现却完全不同,而且相当离奇:

  • ThreadLocalRandom 逐字复制Random 中的一些方法,其他方法稍作修改;当然,大部分代码都可以重复使用。
  • Thread 存储种子和用于初始化 `ThreadLocalRandom 的探针变量,违反封装;
  • ThreadLocalRandom 使用Unsafe 访问Thread 中的变量,我想这是因为这两个类位于不同的包中,但Thread 中的状态变量必须是私有的 - Unsafe 只是因为封装才需要违规;
  • ThreadLocalRandom 将其下一个 nextGaussian 存储在静态 ThreadLocal 中,而不是像 Random 那样存储在实例变量中。

总的来说,我的粗略检查似乎揭示了一个丑陋的 Random 副本,与上面的简单实现相比没有任何优势。但是标准库的作者很聪明,所以这种奇怪的方法一定有一些原因。有没有人知道为什么ThreadLocalRandom 会以这种方式实现?

【问题讨论】:

  • 我还没有看到ThreadLocalRandom 的实现,但我知道parent types should not know of/depend on their subtypes(关于您更简单的替代方案)
  • @VinceEmigh 好吧,两者都可以委托给UnsynchronizedRandom。无论哪种方式,当前实现的主要问题都可以轻松避免。
  • 看起来这个类是用 Java 8 重写的。JDK 1.7u79 版本只有 226 个 LOC,而 OpenJDK 8b132 version 有 1101 个 LOC。 Java 8 版本中类声明之后的代码 cmets 似乎可以解释您的一些问题。
  • Java 8 版本的内部文档中提到了SplittableRandom,其中一些代码是重复的。
  • @dimo414 我问这个问题的真正原因是我认为这段难看的代码一定有充分的理由。如果我不认为 JDK 的作者很好,我会认为这只是糟糕的代码。关于实现细节,他们可以并且确实会在版本之间进行更改。 String.substring 更改为复制 Java 7 中的子字符串,这会影响 Java 应用程序。用于随机数生成的特定算法不应该被如此详细地记录下来,也不应该被完全依赖。我认为这是 Java API 中的许多遗留错误之一。

标签: java random implementation


【解决方案1】:

关键问题是很多代码都是遗留的,不能(轻松)更改 - Random 被设计为通过同步其所有方法来实现“线程安全”。这是可行的,因为Random 的实例可以跨多个线程使用,但这是一个严重的瓶颈,因为没有两个线程可以同时检索随机数据。一个简单的解决方案是构造一个ThreadLocal&lt;Random&gt; 对象,从而避免锁争用,但这仍然不理想。即使在没有争议的情况下,synchronized 方法仍然有一些开销,并且构造 n Random 实例在它们基本上都在做同样的工作时是一种浪费。

所以在高级别的 ThreadLocalRandom 存在作为性能优化,因此它的实现将是“奇怪的”是有道理的,因为 JDK 开发人员已经投入时间来优化它。

JDK 中有很多类,乍一看是“丑陋的”。但是请记住,JDK 作者正在解决与您不同的问题。他们编写的代码将以无数种方式被成千上万甚至数百万的开发人员使用。他们必须定期权衡最佳实践以提高效率,因为他们编写的代码对任务至关重要。

Effective Java: Item 55 也解决了这个问题——关键是优化应该作为最后的手段,由专家进行。 JDK 开发人员这些专家。

针对您的具体问题:

ThreadLocalRandom 逐字复制Random 中的一些方法,其他方法稍作修改;当然,大部分代码都可以重复使用。

很遗憾,没有,因为 Random 上的方法是 synchronized。如果它们被调用ThreadLocalRandom 将导致Random 的锁争用问题。 TLR 需要覆盖每个方法,以便从方法中删除 synchronized 关键字。

Thread 存储种子和用于初始化ThreadLocalRandom 的探针变量,违反封装;

首先,它真的不是“违反封装”,因为该字段仍然是包私有的。它是从用户封装的,这是目标。我不会对此过于纠结,因为这里做出的决定是为了提高性能。有时,性能是以正常的良好设计为代价的。在实践中,这种“违规”是无法检测到的。该行为被简单地封装在两个类中,而不是一个类中。

将种子放在Thread 中允许ThreadLocalRandom 完全无状态(除了initialized 字段,这在很大程度上无关紧要),因此整个应用程序中只需要存在一个实例。

ThreadLocalRandom 使用Unsafe 访问Thread 中的变量,我想这是因为这两个类位于不同的包中,但Thread 中的状态变量必须是私有的 - Unsafe 仅是必要的,因为封装违规;

许多 JDK 类使用Unsafe。它是一种工具,而不是一种罪恶。同样,我只是不会对这个事实感到太紧张。该类被称为Unsafe 以阻止非专业开发人员滥用它。我们相信/希望 JDK 的作者足够聪明,知道什么时候可以安全使用。

ThreadLocalRandom 将其下一个 nextGaussian 存储在静态 ThreadLocal 中,而不是像 Random 那样存储在实例变量中。

由于只有一个 ThreadLocalRandom 的实例,因此无需将其作为实例变量。我想你也可以证明它也不需要是static,但那时你只是在辩论风格。至少使它static 更清楚地使该类本质上是无状态的。与文件中的mentioned 一样,该字段并不是真正需要的,但可以确保与Random 类似的行为。

【讨论】:

  • 不能像StringBuffer那样将同步代码提取到包私有公共基类吗?
  • 在实践中这是不可能的,因为RandomThreadLocalRandom 位于不同的包中。为什么会这样更难说。查看history,该类是作为JSR 166 的一部分引入的,它通常将并发友好的代码放在java.util.concurrent 中,所以也许这样做只是为了保持一致。
猜你喜欢
  • 2020-07-18
  • 1970-01-01
  • 1970-01-01
  • 2018-11-20
  • 1970-01-01
  • 2017-10-27
  • 1970-01-01
  • 2021-11-01
  • 1970-01-01
相关资源
最近更新 更多