Java:易失性隐含订单保证

时间:2011-11-09 11:11:26

标签: java concurrency volatile java-memory-model

我的问题是对此问题的扩展:Volatile guarantees and out-of-order execution

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

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 ,因此它引入了'barrier'之前发生的事件,确保无法进行重新排序。由于状态字段仅在之前写入 初始化字段并且只读 > 初始化<读取/ em>字段,我可以从状态的声明中删除 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();
        }
    }
    

    它还能没问题吗?

2 个答案:

答案 0 :(得分: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之前。

initialized有3次写入:#B,#0和#2。只有#2指定为真。因此,如果#2在#3之后,#3无法读取为真(这可能是因为没有我不完全理解的无法保证),那么就无法达到#4。

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

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

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

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

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

因此,对变量state的所有访问都在以前发生的链中:#A - &gt; #1 - &gt; #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;这对于保证安全发布至关重要:所有线程都会观察到正确初始化的锁存器。

答案 1 :(得分:-1)

  

1。这个推理是否正确?

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

  

2。是否保证写入初始化字段不会          优化了(因为它只是第一次改变)和          “障碍”不会丢失吗?

  

3。假设,CountDownLatch用作而不是标志          这样的初始化程序......

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