volatile 发布保证有多深?

How deep volatile publication guarantees?

众所周知,如果我们有一些对象引用并且这个引用有 final 字段 - 我们将从 final 字段看到所有可到达的字段(至少在构造函数完成时)

示例 1:

class Foo{
    private final Map map;
     Foo(){
         map = new HashMap();
         map.put(1,"object");
     }

     public void bar(){
       System.out.println(map.get(1));
     }
}

据我了解,在这种情况下,我们可以保证 bar() 方法始终输出 object,因为:
1. 我列出了 class Foo 的完整代码并且地图是最终的;
2。如果某些线程将看到 Foo 的引用并且此引用 != null,那么我们可以保证从最终 map 引用值可访问的值将是实际的 .

我也觉得

示例 2:

class Foo {
    private final Map map;
    private Map nonFinalMap;

    Foo() {
        nonFinalMap = new HashMap();
        nonFinalMap.put(2, "ololo");
        map = new HashMap();
        map.put(1, "object");
    }

    public void bar() {
        System.out.println(map.get(1));
    }

    public void bar2() {
        System.out.println(nonFinalMap.get(2));
    }
}

这里我们对 bar() 方法有相同的保证,但是 bar2 可以抛出 NullPointerException 尽管 nonFinalMap 赋值发生在 map 赋值之前。

我想知道 volatile 怎么样:

示例 3:

class Foo{
        private volatile Map map;
         Foo(){
             map = new HashMap();
             map.put(1,"object");
         }

         public void bar(){
           System.out.println(map.get(1));
         }
    }

据我所知,bar() 方法不能抛出 NullPoinerException 但它可以打印 null; (这方面我完全不确定)

示例 4:

class Foo {
    private volatile Map map;
    private Map nonVolatileMap;

    Foo() {
        nonVolatileMap= new HashMap();
        nonVolatileMap.put(2, "ololo");
        map = new HashMap();
        map.put(1, "object");
    }

    public void bar() {
        System.out.println(map.get(1));
    }

    public void bar2() {
        System.out.println(nonFinalMap.get(2));
    }
}

我认为这里我们对 bar() 方法有相同的保证 bar2() 不能抛出 NullPointerException 因为 nonVolatileMap 赋值写入更高的 volatile map 赋值但它可以输出 null


在 Elliott Frisch 评论后添加

通过比赛示例发布:

public class Main {
    private static Foo foo;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                foo = new Foo();
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                while (foo == null) ; // empty loop

                foo.bar();
            }
        }).start();

    }
}

请证明或更正我对代码片段的评论。

在当前Java内存模型领域,volatile不等于final。也就是说,you cannot replace final with volatile,和认为安全施工保障是一样的。值得注意的是,这在理论上是可以发生的:

public class M {
  volatile int x;
  M(int v) { this.x = v; }
  int x() { return x; }
}

// thread 1
m = new M(42);

// thread 2
M lm;
while ((lm = m) == null); // wait for it
print(lm.x()); // allowed to print "0"

因此,在构造函数中写入 volatile 字段并不安全。

直觉:在上面的例子中m有一场比赛。通过使 field M.x volatile 不会消除该种族,只有使 m 本身 volatile 才会有所帮助。换句话说,该示例中的 volatile 修饰符在 错误的位置 是有用的。在安全发布中,您必须有 "writes -> volatile write -> volatile read that observes volatile write -> reads (now observing writes prior the volatile write)",而不是 "volatile write -> write -> read -> volatile read (that does not observe the volatile write)".

知识点 1: 这 属性 意味着我们可以在构造函数中更积极地优化 volatile-s。这证实了可以放松未观察到的易失性存储的直觉(实际上直到具有 non-escaping this 的构造函数完成后才观察到)。

知识点2: 这也意味着你不能安全地初始化volatile变量。将上面示例中的 M 替换为 AtomicInteger,您就会有一个奇怪的 real-life 行为!在一个线程中调用 new AtomicInteger(42),不安全地发布实例,并在另一个线程中执行 get()——你保证观察到 42 吗?如前所述,JMM 表示 "nope"。 Java 内存模型的较新版本努力保证所有初始化的安全构造,以捕获这种情况。还有许多非 x86 端口 have already strengthened this 是安全的。

知识问答 3: Doug Lea:"This final vs volatile issue has led to some twisty constructions in java.util.concurrent to allow 0 as the base/default value in cases where it would not naturally be. This rule sucks and should be changed."

也就是说,这个例子可以做得更巧妙:

public class C {
  int v;
  C(int v) { this.x = v; }
  int x() { return x; }    
}

public class M {
  volatile C c;
  M(int v) { this.c = new C(v); }
  int x() { 
    while (c == null); // wait!
    return c.x();
  }
}

// thread 1
m = new M(42);

// thread 2
M lm;
while ((lm = m) == null); // wait for it
print(lm.x()); // always prints "42"

如果通过 volatile 字段 after volatile read 观察到在构造函数中由 volatile write 写入的值,通常的安全发布规则就会生效。