Java:易失性隐含订单保证

Kov*_*sky 23 java concurrency volatile java-memory-model

我的问题是对此问题的扩展:易失性保证和无序执行

为了使它更具体,让我们假设我们有一个简单的类,它在初始化后可以处于两种状态:

class A {
    private /*volatile?*/ boolean state;
    private volatile boolean initialized = false;

    boolean getState(){
        if (!initialized){
            throw new IllegalStateException();
        }
        return state;
    }

    void setState(boolean newState){
        state = newState;
        initialized = true;
    }
}
Run Code Online (Sandbox Code Playgroud)

初始化的字段被声明为volatile,因此它在"屏障"之前引入,确保无法进行重新排序.由于国家唯一字段写入之前 初始化字段写入和只读初始化场被读取,我可以去除挥发性从申报关键字状态,仍然不会看到一个陈旧的价值.问题是:

  1. 这个推理是否正确?
  2. 是否保证写入初始化字段不会被优化掉(因为它只是第一次改变)并且"屏障"不会丢失?
  3. 假设,CountDownLatch用作初始化器而不是标志,如下所示:

    class A {
        private /*volatile?*/ boolean state;
        private final CountDownLatch initialized = new CountDownLatch(1);
    
        boolean getState() throws InterruptedException {
            initialized.await();
            return state;
        }
    
        void setState(boolean newState){
            state = newState;
            initialized.countdown();
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    它还会好吗?

irr*_*ble 8

你的代码(大多数)是正确的,这是一个常见的习惯用法.

// reproducing your code
class A

    state=false;              //A
    initialized=false;        //B

    boolean state;
    volatile boolean initialized = false;        //0

    void setState(boolean newState)
        state = newState;                        //1
        initialized = true;                      //2

    boolean getState()
        if (!initialized)                        //3
            throw ...;
        return state;                            //4
Run Code Online (Sandbox Code Playgroud)

行#A #B是用于将默认值写入变量的伪代码(也称为字段归零).我们需要将它们包含在严格的分析中.注意#B与#0不同; 两者都被执行了.#B行不被视为易失性写入.

所有变量的所有易失性访问(读/写)都是按顺序排列的.如果达到#4,我们希望确定#2在此顺序中位于#3之前.

有3个写入initialized:#B,#0和#2.只有#2指定为真.因此,如果#2在#3之后,#3无法读取为真(这可能是由于没有我不完全理解的无空隙保证),则无法达到#4.

因此,如果达到#4,#2必须在#3之前(以易失性访问的总顺序).

因此#2 发生在#3 之前(在随后的易失性读取之前发生易失性写入).

通过编程顺序,#1发生在#2之前,#3发生在#4之前.

因此,传递性因此#1发生在#4之前.

线#A,默认写入,发生在所有事情之前(除了其他默认写入)

因此,对变量的所有访问state都在一个先发生的链中:#A - >#1 - >#4.没有数据竞争.程序正确同步.读#4必须遵守写#1

但是有一点问题.第0行显然是多余的,因为#B已经被赋值为false.在实践中,易失性写入在性能上是不可忽略的,因此我们应该避免#0.

更糟糕的是,#0的存在会导致不良行为:#0可能发生在#2之后!因此,它可能会setState()被调用,但随后会getState()继续抛出错误.

如果未安全发布对象,则可以执行此操作.假设线程T1创建对象并发布它; 线程T2获取对象并对其进行调用setState().如果发布不安全,T2可以在T1完成初始化对象之前观察对象的引用.

如果要求A安全发布所有对象,则可以忽略此问题.这是一个合理的要求.它可以隐含地预期.

但如果我们没有#0行,这根本就不是问题.默认写#B必须在#2之前发生,因此只要setState()被调用,所有后续getState()都会观察到initialized==true.

在倒计时锁存器的例子中,initializedfinal; 这对于保证安全发布至关重要:所有线程都会观察到正确初始化的锁存器.

  • @Petar最新的内存模型暗示了这一点.旧的内存模型(在Java5之前)确实没有这样的保证(参见http://g.oswego.edu/dl/cpj/jmm.html)在新模型中,易失性写入/读取就像锁定释放/获取可见性.见http://g.oswego.edu/dl/jmm/cookbook.html (4认同)
  • 你误会了.正式分析得出读#4必须遵守写#1.在非正式的解释中,易失性读取将首先清除缓存.因此,如果第二个线程缓存了`state`,则在读取volatile`initialized`时清除缓存; 随后读取`state`将有效地从主存储器中取出,并查看最后指定的值. (3认同)