不稳定的出版物保证多深?

众所周知,如果我们有一些对象引用并且此引用具有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因为:
我列出了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尽管在map分配之前发生了非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 。 换句话说, 你不能用volatile替换final ,并认为安全构造保证是一样的。 值得注意的是,这理论上可以发生:

 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上有一场比赛。 通过使字段 Mx变得volatile而没有消除这种竞争,只是使m本身volatile会有所帮助。 换句话说,该示例中的volatile修饰符位于错误的位置以便有用。 在安全发布中,您必须具有“写入 – >易失性写入 – >易失性读取,观察易失性写入 – >读取(现在观察易失性写入之前的写入)”,而是“易失性写入 – >写入 – >读取 – > volatile read(不遵守volatile写入)“。

琐事1:这个属性意味着我们可以在构造函数中更积极地优化volatile -s。 这证实了直观的观点,即未观察到的易失性存储(实际上,直到具有非逃逸的构造函数完成后才能观察到)可以放松。

琐事2:这也意味着你无法安全地初始化volatile变量。 在上面的示例中将M替换为AtomicInteger ,您将拥有一种特殊的现实生活行为! 在一个线程中调用new AtomicInteger(42) ,不安全地发布实例,并在另一个线程中执行get() – 你能保证观察到42吗? 如上所述,JMM说“不”。 Java内存模型的较新版本力求保证所有初始化的安全构建,以捕获此案例。 而许多非重要的非x86端口已经加强了这个安全性。

琐事3: Doug Lea :“这个final vs volatile问题导致java.util.concurrent中的一些曲折结构,允许0作为基本/默认值,在它自然不会的情况下。这条规则很糟糕,应该改变。 “

也就是说,这个例子可以变得更加狡猾:

 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 cx(); } } // thread 1 m = new M(42); // thread 2 M lm; while ((lm = m) == null); // wait for it print(lm.x()); // always prints "42" 

如果 volatile读取通过volatile写入传递读取由volatile构造函数写入的值,则通常的安全发布规则启动。