在Effective Jave示例中使用原子引用

dev*_*ull 4 java concurrency multithreading

在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");
            }

        }
Run Code Online (Sandbox Code Playgroud)

令我困惑的是使用AtomicReference.他的解释是:

请注意,初始化字段是原子引用(java.util.concurrent.atomic.AtomicReference).这对于确定面对确定的对手的对象完整性是必要的.在没有这种预防措施的情况下,如果一个线程在第二个线程试图使用它时调用实例上的初始化,则第二个线程可能会看到实例处于不一致状态.

我无法理解这是如何加强对象安全性而不是在不一致的状态下使用它.根据我的理解,如果一个线程运行initialize()而第二个线程运行任何访问器,则当第二个读取x或y字段的值而没有初始化被标记为已完成时,就不会出现这种情况.

我可能在这里看到的其他可能的问题是AtomicReference应该是线程安全的(可能内部有volatile字段).这将确保init变量中的值更改与其他线程立即同步,这将阻止IllegalStateException实际上已完成初始化但执行访问器方法的线程无法看到它.但这是作者正在谈论的事情吗?

我的推理是否正确?或者还有其他解释吗?

ysh*_*vit 6

这是一个很长的答案,听起来你已经掌握了一些问题,所以我正在添加标题,试着让你更容易快进你已经知道的部分.

问题

多线程有点棘手,其中一个棘手的问题是允许编译器/ JVM 在没有同步的情况下跨线程重新排序操作.也就是说,如果线程A执行:

field1 = "hello";
field2 = "world";
Run Code Online (Sandbox Code Playgroud)

和线程B做:

System.out.println(field2);
System.out.println(field1);
Run Code Online (Sandbox Code Playgroud)

那么线程B可能会打印出"world"后跟"null"(假设这field1是最初的).这"不应该"发生的,因为你设置field2field1的代码-所以,如果field2已设置的话,想必field1一定是也?不!允许编译器对事物进行重新排序,以便线程2看到分配如下:

field2 = "world";
field1 = "hello";
Run Code Online (Sandbox Code Playgroud)

(它甚至可以看到field2 = "world"永远不会看到field1 = "hello",或者它永远看不到任何分配或其他可能性.)有多种原因导致这种情况发生:由于编译器如何使用寄存器,它可能更有效,或者它可能是这是一种跨CPU核心共享内存的更有效方法.重点是,这是允许的.

......即使是施工人员

这里更不直观的概念之一是构造函数通常不为重新排序提供任何特殊保证(除了final字段之外).因此,不要将构造函数视为除方法之外的任何东西,并且不要将方法视为除了一组操作之外的任何其他方法,并且不要将对象的状态视为除字段分组之外的任何其他内容.很明显,拥有该对象的任何人都可以看到构造函数中的赋值(毕竟,在完成对象之前,如何读取对象的状态?),但由于重新排序,该概念不正确.您的想法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是不会允许这样做上面的重新排序.

特别:

  • 如果线程A执行操作A.
  • 并且线程B执行操作B.
  • 和行动A发生在行动B之前
  • 然后,当线程B执行动作B时,它必须至少看到线程A在动作A中看到的所有动作.换句话说,线程B看到世界至少像线程A看到的那样"最新".

这是非常抽象的,所以让我更具体.建立事先发生边缘的一种方法是使用volatile字段:在写入该字段的一个线程与从该字段读取的另一个线程之间存在HB关系.因此,如果线程A写入一个volatile字段,并且线程B从同一个字段读取,那么线程B必须将该世界视为线程A在写入时看到它(好吧,至少最近那个:线程B可以看到一些后续行动).

所以,让我们说field2volatile.在这种情况下:

Thread 1:
field1 = "hello";
field2 = "world"; // point 1

Thread 2:
System.out.println(field2); // point 2
System.out.println(field1); // point 3
Run Code Online (Sandbox Code Playgroud)

在这里,第1点"开始"HB点关系,点2"完成".这意味着从第2点开始,线程2必须看到线程1在第1点看到的所有内容 - 特别是分配field1 = "hello"(以及field2 = "world").因此,线程2将按\n预期打印出"世界你好".

AtomicReferences

那么,所有这些与这有什么关系AtomicReference呢?秘密在于java.util.concurrent.atomic包的javadoc :

访问和更新原子的记忆效应通常遵循挥发性规则,如Java™语言规范第17.4节所述.

换句话说,myAtomicRef.set和之间存在HB关系myAtomicRef.get.或者,如上例所示,在myAtomicRef.compareAndSet和之间myAtomicRef.get.

回到 AbstractFoo

没有这些AtomicReference行动,就没有建立HB关系AbstractFoo.如果一个线程分配一个值this.x(就像它所做的那样initialize,由构造函数调用)而另一个线程读取该值this.x(就像它在那里一样getX),你可能会遇到上面提到的重新排序问题,并且getX返回默认值x(即,0).

AbstractFoo 采取具体措施,建立HB关系:initialize也叫init.set 其分配this.x = xgetX调用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的强制性引用,JCIP是一本关于多线程问题的好书,特别是它对Java的适用性.


Art*_*ool 0

一方面,AtomicReference提供了happens-before机制,这就是为什么任何线程在一个线程调用init.set(State.INITIALIZED);并且查询访问器的线程调用后都会获得完全初始化的对象init.get()。另一方面,以compareAndSet原子方式工作,这就是为什么只有一个线程并且只能运行一次初始化。作为奖励:java 原子原语是非阻塞的,这就是为什么不仅仅是synchronized.