23

我的问题是对这个问题的扩展:易失性保证和乱序执行

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

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;
    }
}

初始化的字段被声明为volatile,因此它引入了happens-before 'barrier',以确保不会发生重新排序。由于状态字段仅在写入初始化字段之前 写入并且仅读取初始化字段后读取,因此我可以从状态声明中删除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();
        }
    }
    

    还会好吗?

4

2 回答 2

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

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

所有变量上的所有易失性访问(读/写)都是按总顺序排列的。如果达到#4,我们要确定#2 按此顺序在#3 之前。

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

因此,如果达到 #4,则 #2 必须在 #3 之前(在 volatile 访问的总顺序中)。

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

按照编程顺序,#1 发生在#2 之前,#3 发生在#4 之前。

通过传递性,因此#1 发生在#4 之前。

Line#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; 这对于保证安全发布至关重要:所有线程都将观察到正确初始化的锁存器。

于 2011-11-09T17:01:00.987 回答
-1

1. 这个推理正确吗?

不,状态将缓存在线程中,因此您无法获取最新值。

2. 是否保证对初始化字段的写入不会被优化掉(因为它只是第一次更改)并且“障碍”不会丢失?

是的

3. 假设像这样使用 CountDownLatch 而不是标志,作为初始化器...

就像@ratchet 怪胎提到的那样, CountDownLatch 是一次性锁存器,而volatile是一种可重复使用的锁存器,所以第三个问题的答案应该是:如果你要多次设置状态,你应该使用volatile

于 2011-11-09T11:37:17.820 回答