在 Effective Java - Item 74 中,Joshua Bloch 在以下代码片段中演示了带单独初始化方法的无参数构造函数的安全使用。
abstract class AbstractFoo {
private int x, y; // Our state
// This enum and field are used to track initialization
private enum State {
NEW, INITIALIZING, INITIALIZED
};
private final AtomicReference<State> init = new AtomicReference<State>(
State.NEW);
public AbstractFoo(int x, int y) {
initialize(x, y);
}
// This constructor and the following method allow
// subclass's readObject method to initialize our state.
protected AbstractFoo() {
}
protected final void initialize(int x, int y) {
if (!init.compareAndSet(State.NEW, State.INITIALIZING))
throw new IllegalStateException("Already initialized");
this.x = x;
this.y = y;
// ...Do anything else the original constructor did
init.set(State.INITIALIZED);
}
// These methods provide access to internal state so it can
// be manually serialized by subclass's writeObject method.
protected final int getX() {
checkInit();
return x;
}
protected final int getY() {
checkInit();
return y;
}
// Must call from all public and protected instance methods
private void checkInit() {
if (init.get() != State.INITIALIZED)
throw new IllegalStateException("Uninitialized");
}
}
令我困惑的是
AtomicReference
的使用.他的解释听起来:Note that the initialized field is an atomic reference (java.util.concurrent.atomic.AtomicReference). This is necessary to ensure object integrity in the face of a determined adversary. In the absence of this precaution, if one thread were to invoke initialize on an instance while a second thread attempted to use it, the second thread might see the instance in an inconsistent state.
我不明白这如何增强对象安全性,防止在不一致的状态下使用它。据我了解,如果一个线程运行
initialize()
而第二个运行任何访问器,不可能有这样的情况第二个将读取 x 或 y 字段的值,而不会将初始化标记为已完成。
我可能在这里看到的其他可能问题是
AtomicReference
应该是线程安全的(可能里面有 volatile 字段)。这将确保立即同步 init
中的值更改与其他线程的变量会阻止获取 IllegalStateException
实际上初始化已经完成但执行访问器方法的线程看不到它。但这就是作者所说的吗?我的推理正确吗?或者对此有其他解释吗?
最佳答案
这是一个很长的答案,听起来您已经对这个问题有所了解,因此我添加了标题以尝试让您更轻松地快进您已经知道的部分。
问题
多线程有点棘手,其中一个棘手的地方是编译器/JVM 允许重新订购 在没有同步的情况下跨线程操作。也就是说,如果线程 A 执行以下操作:
field1 = "hello";
field2 = "world";
线程 B 执行以下操作:
System.out.println(field2);
System.out.println(field1);
然后线程 B 可能会打印出“world”后跟“null”(假设这就是
field1
最初的样子)。这“不应该”发生,因为您设置了 field2
之后 field1
在代码中——所以如果 field2
已经设置,那么肯定field1
也一定是?不!允许编译器对事物重新排序,以便线程 2 将分配视为如下发生:field2 = "world";
field1 = "hello";
(它甚至可以看到
field2 = "world"
而永远看不到 field1 = "hello"
,或者它永远看不到赋值或其他可能性。)发生这种情况的原因有多种:由于编译器想要使用的方式,它可能更有效寄存器,或者它可能是一种更有效的跨 CPU 内核共享内存的方式。重点是,这是允许的。...即使有构造函数
这里比较不直观的概念之一是构造函数通常不为重新排序提供任何特殊保证( except, it does for
final
fields )。因此,不要将构造函数视为方法以外的任何东西,不要将方法视为一组 Action 以外的任何东西,也不要将对象的状态视为一组字段以外的任何东西。很明显,任何拥有该对象的人都可以看到构造函数中的赋值(毕竟,在完成创建对象之前,您如何读取对象的状态?),但由于重新排序,该概念是不正确的。您认为的 foo = new ConcreteFoo()
实际上是:ConcreteFoo
分配内存(称之为 this
);调用 initalize
,做些事情... this.x = x
this.y = y
foo = <the newly constructed object>
您可以看到底部三个作业如何重新排序;线程 B 可以看到它们以各种方式发生,包括(但不限于):
foo = <the newly constructed object, with default values for all fields>
foo.getX()
返回 0
this.x = x
(可能很久以后)this.y = y
永远不会被线程 B 看到) 发生在关系之前
然而,有办法解决这个问题。让我们把
AtomicReference
一会儿……解决问题的方法是用一个 发生在 (HB) 关系。如果写入和读取之间存在 HB 关系,则不允许 CPU 进行上述重新排序。
具体来说:
这很抽象,所以让我让它更具体。建立先发生边缘的一种方法是使用
volatile
字段:一个线程写入该字段与另一个线程读取该字段之间存在 HB 关系。因此,如果线程 A 写入 volatile
字段,并且线程 B 从同一字段读取,然后线程 B 必须看到世界,就像线程 A 在写入时看到的一样(好吧,至少最近这样:线程 B 也可以看到一些后续操作)。所以,让我们说
field2
分别是 volatile
.在这种情况下:Thread 1:
field1 = "hello";
field2 = "world"; // point 1
Thread 2:
System.out.println(field2); // point 2
System.out.println(field1); // point 3
在这里,第 1 点“开始”了第 2 点“结束”的 HB 关系。这意味着从第 2 点开始,线程 2 必须看到线程 1 在第 1 点看到的所有内容——特别是赋值
field1 = "hello"
(以及 field2 = "world"
)。因此,线程 2 将按预期打印出“world \n
hello”。原子引用
那么,这一切与
AtomicReference
有什么关系? ? secret 在于 java.util.concurrent.atomic
的 javadoc包裹:The memory effects for accesses and updates of atomics generally follow the rules for volatiles, as stated in section 17.4 of The Java™ Language Specification.
换句话说,
myAtomicRef.set
之间存在HB关系。和 myAtomicRef.get
.或者,如上例所示,在 myAtomicRef.compareAndSet
之间和 myAtomicRef.get
.返回
AbstractFoo
没有
AtomicReference
操作,在 AbstractFoo
中没有建立 HB 关系.如果一个线程为 this.x
赋值(就像在 initialize
中一样,由构造函数调用)并且另一个线程读取值 this.x
(就像在 getX
期间一样),您可能会遇到上面提到的重新排序问题,并且有 getX
返回 x
的默认值(即 0
)。但是
AbstractFoo
确实采取了具体措施建立HB关系:initialize
还调用init.set
分配后 this.x = x
, 和 getX
电话init.get
(通过 checkInit
)在它读取 this.x
之前返回它(类似于 y
)。这建立了 HB 关系,确保线程 2 调用 getX
,当它读到 this.x
,看世界就像线程A在initialize
结尾看到的一样,当它调用 init.set
;具体来说,线程 2 看到操作 this.x = x
在它执行操作之前 return [this.]x
.进一步阅读
还有一些其他方法可以建立发生在边缘之前,但这超出了这个答案的范围。它们列在 JLS 17.4.4 .
以及对 JCIP 的强制性引用,一本关于多线程问题的好书,特别是它们对 Java 的适用性。
关于java - 在 Effective Java 示例中使用原子引用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22364309/