Java:不稳定的隐含订单保证

我的问题是这个问题的扩展:不稳定的保证和无序执行

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

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 关键字,并且仍然永远不会看到过时的值。问题是:

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

    还没事吗?


答案 1

你的代码(大部分)是正确的,这是一个常见的习语。

// 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个写信:#B,#0和#2。只有 #2 赋值为 true。因此,如果#2在#3之后,#3不能读真(这可能是由于没有我不完全理解的无机保证),那么#4就无法到达。initialized

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

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

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

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

行#A,默认写入,在所有内容之前发生(其他默认写入除外)

因此,对变量的所有访问都位于一个“发生前”链中:#A -> #1 -> #4。没有数据竞赛。程序已正确同步。阅读 #4 必须遵守写入 #1state

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

更糟糕的是,#0的存在可能会导致不希望的行为:#0可能发生在#2之后!因此,可能会发生调用,但随后不断抛出错误。setState()getState()

如果未安全地发布对象,则可能会发生这种情况。假设线程 T1 创建对象并发布它;线程 T2 获取对象并调用它。如果发布不安全,则在 T1 完成对象初始化之前,T2 可以观察对对象的引用。setState()

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

但是,如果我们没有#0行,这根本不是问题。默认写入#B必须发生在 #2 之前,因此只要调用 is,所有后续都将遵守 。setState()getState()initialized==true

在倒计时闩锁示例中,是 ;这对于保证安全发布至关重要:所有线程都将观察到正确初始化的闩锁。initializedfinal


答案 2

1. 这个推理是否正确?

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

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

是的

3. 假设,像这样,一个 CountDownLatch 被用作初始值设定项,而不是标志...

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


推荐