在 Java 中完全初始化后是否发布不可变对象?
Are immutable objects published after they are fully initialized in Java?
我正在阅读 Fred Long 在 Java 并发指南 中有关不可变对象和线程安全的内容。
这是书中的一段代码。
// Immutable Helper
public final class Helper {
private final int n;
public Helper(int n) {
this.n = n;
}
// ...
}
// and a mutable Foo class:
final class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void setHelper(int num) {
helper = new Helper(num);
}
}
代码段后面有解释:
The getHelper() method publishes the mutable helper field. Because the
Helper class is immutable, it cannot be changed after it is
initialized. Furthermore, because Helper is immutable, it is always
constructed properly before its reference is made visible in
compliance with guideline “TSM03-J. Do not publish partially
initialized objects” on page 162.
现在让我们打开他们提到的第 162 页。这是另一个代码片段。
class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void initialize() {
helper = new Helper(42);
}
}
public class Helper {
private int n;
public Helper(int n) {
this.n = n;
}
}
后面有自己的解释:
If a thread accesses helper using the getHelper() method before the
initialize() method has been called, the thread will observe an
uninitialized helper field. Later, if one thread calls initialize()
and another calls getHelper(), the second thread might observe one of
the following:
- the helper reference as NULL,
- a fully initialized Helper object with the n field set to 42,
- a partially initialized Helper object with an uninitialized n that contains the default value 0.
所以我加粗的两个陈述是不是相互矛盾?
A 写了一段代码来测试案例,运行 试了几次,都没有得到 0。只有 null 或 42。这是我的代码:
package com.sample;
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Foo foo = new Foo();
Initer initer = new Initer(foo);
Getter getter = new Getter(foo);
initer.start();
getter.start();
}
}
static class Getter extends Thread {
private Foo foo;
Getter(Foo foo) {
this.foo = foo;
}
@Override
public void run() {
System.out.println(foo.getHelper());
}
}
static class Initer extends Thread {
private Foo foo;
Initer(Foo foo) {
this.foo = foo;
}
@Override
public void run() {
foo.initialize();
}
}
static class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void initialize() {
helper = new Helper(42);
}
}
public static class Helper {
private int n;
public Helper(int n) {
this.n = n;
}
@Override
public String toString() {
return Integer.toString(n);
}
}
}
除非该字段是 final
,否则无法保证该值可见,
- 不能保证它不会。
- 一旦代码预热,即在 10,000 次迭代之后,您更有可能看到此问题。
- 您正在使用
println
,它有自己的内存屏障,这将使您更难发现任何不一致之处。
两种说法并不矛盾。如果第一个示例字段 n
是 final
。这就是 Helper
class 不可变的原因。在第二个示例中,n
不是最终的,Helper
不是不可变的,这意味着可以返回部分初始化的实例。
就用您的代码观察这一点而言,Peter 是对的,由于多种原因很难在实践中进行测试。
我正在阅读 Fred Long 在 Java 并发指南 中有关不可变对象和线程安全的内容。
这是书中的一段代码。
// Immutable Helper
public final class Helper {
private final int n;
public Helper(int n) {
this.n = n;
}
// ...
}
// and a mutable Foo class:
final class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void setHelper(int num) {
helper = new Helper(num);
}
}
代码段后面有解释:
The getHelper() method publishes the mutable helper field. Because the Helper class is immutable, it cannot be changed after it is initialized. Furthermore, because Helper is immutable, it is always constructed properly before its reference is made visible in compliance with guideline “TSM03-J. Do not publish partially initialized objects” on page 162.
现在让我们打开他们提到的第 162 页。这是另一个代码片段。
class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void initialize() {
helper = new Helper(42);
}
}
public class Helper {
private int n;
public Helper(int n) {
this.n = n;
}
}
后面有自己的解释:
If a thread accesses helper using the getHelper() method before the initialize() method has been called, the thread will observe an uninitialized helper field. Later, if one thread calls initialize() and another calls getHelper(), the second thread might observe one of the following:
- the helper reference as NULL,
- a fully initialized Helper object with the n field set to 42,
- a partially initialized Helper object with an uninitialized n that contains the default value 0.
所以我加粗的两个陈述是不是相互矛盾?
A 写了一段代码来测试案例,运行 试了几次,都没有得到 0。只有 null 或 42。这是我的代码:
package com.sample;
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Foo foo = new Foo();
Initer initer = new Initer(foo);
Getter getter = new Getter(foo);
initer.start();
getter.start();
}
}
static class Getter extends Thread {
private Foo foo;
Getter(Foo foo) {
this.foo = foo;
}
@Override
public void run() {
System.out.println(foo.getHelper());
}
}
static class Initer extends Thread {
private Foo foo;
Initer(Foo foo) {
this.foo = foo;
}
@Override
public void run() {
foo.initialize();
}
}
static class Foo {
private Helper helper;
public Helper getHelper() {
return helper;
}
public void initialize() {
helper = new Helper(42);
}
}
public static class Helper {
private int n;
public Helper(int n) {
this.n = n;
}
@Override
public String toString() {
return Integer.toString(n);
}
}
}
除非该字段是 final
,否则无法保证该值可见,
- 不能保证它不会。
- 一旦代码预热,即在 10,000 次迭代之后,您更有可能看到此问题。
- 您正在使用
println
,它有自己的内存屏障,这将使您更难发现任何不一致之处。
两种说法并不矛盾。如果第一个示例字段 n
是 final
。这就是 Helper
class 不可变的原因。在第二个示例中,n
不是最终的,Helper
不是不可变的,这意味着可以返回部分初始化的实例。
就用您的代码观察这一点而言,Peter 是对的,由于多种原因很难在实践中进行测试。