理解为什么根据 Java 内存模型在构造函数中启动线程是不安全的
Understanding why is it unsafe to start a thread inside a constructor in terms of the Java memory model
根据 Java 并发实践,在 class 构造函数中启动线程是危险的。原因是这会在对象完全构造之前将 this
指针暴露给另一个线程。
尽管之前的许多 Whosebug 问题都讨论过这个话题,但我仍然难以理解为什么这是一个如此令人担忧的问题。特别是,我希望从 Java 内存模型的角度澄清在构造函数中启动线程是否会导致内存一致性问题。
让我举一个具体的例子来说明我想做的事情。 (这段代码的期望输出是将数字 20 打印到控制台。)
private static class ValueHolder {
private int value;
private Thread thread;
ValueHolder() {
this.value = 10;
thread = new Thread(new DoublingTask(this)); // exposing "this" pointer!!!
thread.start(); // starting thread inside constructor!!!
}
int getValue() {
return value;
}
void awaitTermination() {
try {
thread.join();
} catch (InterruptedException ex) {}
}
}
private static class DoublingTask implements Runnable {
private ValueHolder valueHolder;
DoublingTask(ValueHolder valueHolder) {
this.valueHolder = valueHolder;
}
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(valueHolder.getValue() * 2); // I expect to print out 20...
}
}
public static void main(String[] args) {
ValueHolder myValueHolder = new ValueHolder();
myValueHolder.awaitTermination();
}
是的,我知道线程在我们从构造函数 return 之前启动。是的,我知道 this
指针暴露给了线程。 然而,我确信代码是正确的。我相信它总是会向控制台打印数字 20。
- 赋值
this.value = 10
发生在thread.start()
之前。 (这是因为 this.value = 10
在程序顺序中位于 thread.start()
之前。)
- 主线程中
thread.start()
的调用发生在新创建线程中.run()
方法开始之前。 (因为thread.start()
是一个同步动作。)
.run()
方法的开始发生在 System.out.println(valueHolder.getValue() * 2);
打印语句之前。 (同样,按节目顺序。)
因此,根据Java内存模型,打印语句应该读取valueHolder.value
的正确初始化值(即10)。因此,尽管忽略了 Java 并发实践 的建议,我似乎仍然编写了一段正确的代码。
我弄错了吗?我错过了什么?
UPDATE:根据回答和评论,我现在明白我的代码示例 由于我提供的原因在功能上是正确的。但是,以这种方式编写代码是一种不好的做法,因为其他开发人员将来有可能在线程启动后添加更多的初始化语句。可能出现此类错误的一种情况是在实现此 class.
的子 classes 时
假设我分class 你的class。它可能在需要时尚未初始化其字段。
class BetterValueHolder extends ValueHolder
{
private int betterValue;
BetterValueHolder(final int betterValue)
{
// not actually required, it's added implicitly anyway.
// just to demonstrate where your constructor is called from
super();
try
{
Thread.sleep(1000); // just to demonstrate the race condition more clearly
}
catch (InterruptedException e) {}
this.betterValue = betterValue;
}
@Override
public int getValue()
{
return betterValue;
}
}
这将打印零,无论为 BetterValueHolder
.
的构造函数提供什么值
根据 Java 并发实践,在 class 构造函数中启动线程是危险的。原因是这会在对象完全构造之前将 this
指针暴露给另一个线程。
尽管之前的许多 Whosebug 问题都讨论过这个话题,但我仍然难以理解为什么这是一个如此令人担忧的问题。特别是,我希望从 Java 内存模型的角度澄清在构造函数中启动线程是否会导致内存一致性问题。
让我举一个具体的例子来说明我想做的事情。 (这段代码的期望输出是将数字 20 打印到控制台。)
private static class ValueHolder {
private int value;
private Thread thread;
ValueHolder() {
this.value = 10;
thread = new Thread(new DoublingTask(this)); // exposing "this" pointer!!!
thread.start(); // starting thread inside constructor!!!
}
int getValue() {
return value;
}
void awaitTermination() {
try {
thread.join();
} catch (InterruptedException ex) {}
}
}
private static class DoublingTask implements Runnable {
private ValueHolder valueHolder;
DoublingTask(ValueHolder valueHolder) {
this.valueHolder = valueHolder;
}
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(valueHolder.getValue() * 2); // I expect to print out 20...
}
}
public static void main(String[] args) {
ValueHolder myValueHolder = new ValueHolder();
myValueHolder.awaitTermination();
}
是的,我知道线程在我们从构造函数 return 之前启动。是的,我知道 this
指针暴露给了线程。 然而,我确信代码是正确的。我相信它总是会向控制台打印数字 20。
- 赋值
this.value = 10
发生在thread.start()
之前。 (这是因为this.value = 10
在程序顺序中位于thread.start()
之前。) - 主线程中
thread.start()
的调用发生在新创建线程中.run()
方法开始之前。 (因为thread.start()
是一个同步动作。) .run()
方法的开始发生在System.out.println(valueHolder.getValue() * 2);
打印语句之前。 (同样,按节目顺序。)
因此,根据Java内存模型,打印语句应该读取valueHolder.value
的正确初始化值(即10)。因此,尽管忽略了 Java 并发实践 的建议,我似乎仍然编写了一段正确的代码。
我弄错了吗?我错过了什么?
UPDATE:根据回答和评论,我现在明白我的代码示例 由于我提供的原因在功能上是正确的。但是,以这种方式编写代码是一种不好的做法,因为其他开发人员将来有可能在线程启动后添加更多的初始化语句。可能出现此类错误的一种情况是在实现此 class.
的子 classes 时假设我分class 你的class。它可能在需要时尚未初始化其字段。
class BetterValueHolder extends ValueHolder
{
private int betterValue;
BetterValueHolder(final int betterValue)
{
// not actually required, it's added implicitly anyway.
// just to demonstrate where your constructor is called from
super();
try
{
Thread.sleep(1000); // just to demonstrate the race condition more clearly
}
catch (InterruptedException e) {}
this.betterValue = betterValue;
}
@Override
public int getValue()
{
return betterValue;
}
}
这将打印零,无论为 BetterValueHolder
.