【问题标题】:Can I launch a thread from a constructor?我可以从构造函数启动线程吗?
【发布时间】:2009-05-11 21:26:11
【问题描述】:

鉴于 J5+ 内存模型 (JSR-133),以下代码是线程安全且允许的吗?

如果它是安全的,在某些情况下是否可以接受?

public final class BackgroundProcessor
extends Object
implements Runnable
{

public BackgroundProcessor(...) {
    super();

    ...

    new Thread(this).start();
    }

public void run() {
    ...
    }
}

当我阅读新的 JMM 规范时,启动线程会创建与启动线程所做的任何事情的发生前关系。

假设对象在构造函数中设置了私有成员变量并在run()中使用。

并且该类被标记为 final 以防止子类意外。

注意:这里有一个类似的问题,但角度不同:calling thread.start() within its own constructor

【问题讨论】:

  • 我建议除了初始化字段之外,在构造函数中做任何事情都不是一个好主意。这将使您的代码很难测试。这是一篇有用的文章,比我能更好地解释这一点:misko.hevery.com/code-reviewers-guide。我最初把它作为一个答案,但它实际上并没有回答你的问题,因此它是一个评论。
  • @bm212:很好的参考(+1)。虽然,这是一个特例构造,我试图确定是否需要重构,以及在什么优先级。

标签: java multithreading


【解决方案1】:

JLS: Threads and Locks

两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。

如果我们有两个动作 x 和 y,我们写 hb(x, y) 来表示 x 发生在 y 之前。

  • 如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

线程上的 start() 调用发生在已启动线程中的任何操作之前。

所以我们可以得出结论,在调用 Thread.start() 之前,构造函数中的所有操作都发生在线程中启动的所有操作之前。如果 Thread.start() 是对对象字段的所有写入,则启动的线程将看到对对象字段的所有写入。如果没有其他对字段的写入(另一个线程将读取),则代码是线程安全的。

【讨论】:

  • 是的,我也是这样读的,但它似乎违反了在构建过程中不应让对“this”的引用逃逸的警告(在少数情况下,我已经这样做了,我觉得还可以,我不经常这样做)。在这种情况下,我正在寻找线程正确性的确认,而不是好的或坏的风格 - 前者是修复的高优先级,后者不是。
  • 我认为您声明类 final 的事实意味着这将起作用。但是,如果您(或其他任何人)曾经删除该最终声明,则您可能会冒着不安全地发布您的对象的风险。我会把重构作为一个原则问题,而不是必要性。
【解决方案2】:

好吧,除非您特别想在 Java 并发问题上不同意 Brian Goetz,否则我认为这是错误的。虽然他的原话是“最好不要……”,但我还是听从了他的建议。来自 JCIP,p。 42:

“一个常见错误可以让 'this' 参考转义期间 构造是启动一个线程 来自构造函数 [...] 创建一个线程没有错 在构造函数中,但最好不要 立即启动线程。 相反,公开一个开始或初始化 方法……”

更新:只是为了详细说明为什么会出现这个问题。尽管可以保证在调用 Thread.start() 和该线程的 run() 方法实际开始执行之间存在内存屏障,但在构造函数 after 调用线程.start()。因此,如果仍然要设置其他一些变量,或者如果 JVM 执行一些“刚刚构造的对象”的内务处理,则不能保证其他线程可以看到这些变量:即对象可以被其他线程处于不完整状态。

顺便说一句,除了它是否真的破坏了 JMM 之外,它也有点像构造函数中的“奇怪的事情”。

【讨论】:

  • 是的,但为什么不好?如果它被破坏了,我会期待像“你不能在你的构造函数中启动一个线程”这样的措辞,而不是“最好不要......”。根据 Esko 的回答,我倾向于认为 JMM 规范将在构造函数末尾开始的线程定义为线程安全的。另外,这句话是 WRT 修订后的 JMM 还是旧的?
  • Goetz 指的是新 JMM 还是旧 JMM 的问题很重要,因为 JSR-133 添加的一件事是使 Thread.start() 和 Thread.join() 显式发生- 边界之前。
  • 绝对是新的内存模型——这正是本书的重点。 Thread.start() 确实构成了之前发生的边界,但只是在 Thread.start() 调用之前的代码(即不一定存储在构造函数期间设置的所有对象状态)发生在线程运行之前( ) 方法。
  • @Niel:在线程启动之后没有代码,我的问题是特定于示例代码,其中线程作为构造函数中的最后一件事启动(顺便说一句:我同意这样做很奇怪,但在我做过的几个地方确实有意义)。
  • 好吧,请注意您正在制造一个定时炸弹。当构造函数完成时,您正在运行的 JVM 可能会执行一些“对象提交”过程(例如,提交最终字段的值);我至少会发表评论以提醒您代码的未来维护者意外添加这样一个字段,并且在您升级 VM 时我会仔细测试...
【解决方案3】:
  1. 您的类可能应该实现 Runnable

  2. 是的,你的代码没问题

  3. 不需要扩展 Object.我确定您知道 Object 被所有类隐式扩展。我不知道这种显式扩展对象的流行做法来自哪里,但这是一种糟糕的风格。

【讨论】:

  • @Roman: (1) 是 - 哎呀; (2) 我认为是的; (3) 糟糕的风格只有你有权获得的 IYO(我相信很多人都会分享一个观点 - 我没有,但我的编程风格不是这篇文章的主题)。
猜你喜欢
  • 2020-02-09
  • 2010-10-18
  • 1970-01-01
  • 2011-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-23
相关资源
最近更新 更多