在Effective Java中 - 第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
。他的解释听起来是:
请注意,初始化字段是原子引用 (java.util.concurrent.atomic.AtomicReference中)。这是必要的 面对坚定的对手,确保对象的完整性。在里面 没有这种预防措施,如果一个线程要调用初始化 第二个线程尝试使用它时的实例,第二个 线程可能会看到实例处于不一致状态。
我无法理解这是如何加强对象安全性以防止在不一致的状态下使用它。根据我的理解,如果一个线程运行initialize()
而第二个线程运行任何访问器,则不会出现这种情况
第二个将读取x或y字段的值,而不将初始化标记为已完成。
我可能会在这里看到的其他可能的问题是AtomicReference
应该是线程安全的(可能内部有volatile字段)。这将确保init
变量中的值更改与其他线程立即同步,这会阻止获得IllegalStateException
实际上已完成初始化但执行访问器方法的线程无法看到它。但这是作者正在谈论的事情吗?
我的推理是否正确?或者还有其他解释吗?
答案 0 :(得分:6)
这是一个很长的答案,听起来你已经掌握了这个问题,所以我要添加标题来尝试让你更容易快进你已经知道的部分。< / p>
多线程有点棘手,其中一个棘手的问题是,在没有同步的情况下,允许编译器/ JVM跨线程重新排序操作。也就是说,如果线程A执行:
field1 = "hello";
field2 = "world";
和线程B确实:
System.out.println(field2);
System.out.println(field1);
然后,线程B可能打印出来&#34; world&#34;接着是&#34; null&#34; (假设field1
最初的是什么。这个&#34;不应该&#34;会发生这种情况,因为您在代码中field2
之后设置了field1
- 所以如果设置了field2
,那么肯定field1
也必须如此?不!允许编译器重新排序,以便线程2看到分配如下:
field2 = "world";
field1 = "hello";
(它甚至可以看到field2 = "world"
而永远不会看到field1 = "hello"
,或者它永远看不到任务或其他可能性。)有多种原因导致这种情况发生:由于编译器想要使用寄存器,它可能更有效,或者它可能是跨CPU核心共享内存的更有效方式。重点是,它是允许的。
这里一个更不直观的概念是构造函数通常不会为重新排序(except, it does for final
fields)提供任何特殊保证。因此,不要将构造函数视为除方法之外的任何其他方法,并且不要将方法视为除了一组操作之外的其他方法,并且不要考虑对象的问题。除了一组字段之外的任何其他状态。很明显,拥有该对象的任何人都可以看到构造函数中的赋值(毕竟,在完成对象之前,如何读取对象的状态?),但由于重新排序,这个概念是不正确的。您认为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
但是,有办法解决这个问题。让我们暂时将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点和第34点开始&#34; HB关系指向2&#34;完成。&#34;这意味着从第2点开始,线程2必须看到线程1在第1点看到的所有内容 - 特别是,赋值field1 = "hello"
(以及field2 = "world"
)。因此,线程2将打印出#34; world \n
你好&#34;如预期的那样。
那么,所有这些与AtomicReference
有什么关系?秘密在于java.util.concurrent.atomic
包的javadoc:
访问和更新原子的记忆效应通常遵循挥发性规则,如Java™语言规范第17.4节所述。
换句话说,myAtomicRef.set
和myAtomicRef.get
之间存在HB关系。或者,如上例所示,在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
{{1 }}和getX
调用init.get
(通过checkInit
)之前它会读取this.x
以返回它(类似于y
) 。这确定了HB关系,确保线程2在读取getX
时,将this.x
视为线程A在initialize
结束时看到它,当它调用init.set
时{1}};具体而言,线程2在执行操作this.x = x
之前会看到操作return [this.]x
。
还有一些其他方法可以建立先发生的边缘,但这个方法超出了这个答案的范围。它们列在JLS 17.4.4。
中对JCIP的强制性引用,这是一本关于多线程问题的好书,特别是它对Java的适用性。
答案 1 :(得分:0)
一方面,AtomicReference提供了before-before机制,这就是为什么在一个线程调用init.set(State.INITIALIZED);
之后任何线程将获得完全初始化的对象,并且查询访问者的线程调用{{1 }}。
另一方面,init.get()
原子地工作,这就是为什么只有一个线程只能运行一次初始化的原因。作为奖励:java原子基元是非阻塞的,这就是为什么不只是compareAndSet
。
答案 2 :(得分:-1)
如果您尝试使用非arg构造函数创建实例并调用&#34; initialize&#34;来自一个线程的方法,并尝试使用来自不同线程的对象(如getX())中的方法。没有AtomicReference,有可能&#34; checkInit&#34;即使对象被正确初始化,方法也可以抛出异常,因为无法确保更改为&#34; state&#34;在线程中可见。 AtomicReference同步访问&#34; state&#34;所以任何方法都可以获得适当的值。
因此,作为总结,您是正确的,如果&#34;初始化&#34;那么访问者将无法工作。没有运行,但如果不使用AtomicReference,访问者仍有可能抛出异常。