不稳定的出版物保证多深?
众所周知,如果我们有一些对象引用并且此引用具有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构造函数写入的值,则通常的安全发布规则启动。