众所周知,如果我们有一些对象引用并且此引用具有final字段 - 我们将看到来自final字段的所有可到达字段(至少在构造函数完成时)
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.我列出了班级Foo
的完整代码,地图是最终的;
的 2。如果某个线程会看到Foo
的引用和此引用!= null,那么我们保证可以从最终map
参考值到达。
我也认为
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
分配。
我想知道如何挥发:
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
; (我完全不确定这方面)
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
添加
通过比赛示例发布:
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();
}
}
请将我的评论改为或更正为代码段。
答案 0 :(得分:22)
在当前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
上有一场比赛。通过制作字段 M.x
volatile
不会消除这种竞争,只会使m
本身volatile
有所帮助。换句话说,该示例中的volatile
修饰符位于错误位置有用。在安全发布中,您必须具有“写入 - >易失性写入 - >易失性读取,观察易失性写入 - >读取(现在观察易失性写入之前的写入)”,而是“易失性写入 - >写入” - > read - > volatile read(不遵守volatile写入)“。
琐事1:此属性意味着我们可以在构造函数中更积极地优化volatile
。这证实了直觉,即未观察到的易失性存储(实际上,直到具有非逃逸this
完成的构造函数才会被忽略)。
琐事2:这也意味着您无法安全地初始化volatile
个变量。在上面的示例中将M
替换为AtomicInteger
,您就会有一种特殊的现实生活行为!在一个线程中调用new AtomicInteger(42)
,不安全地发布实例,在另一个线程中执行get()
- 您是否可以保证观察42
?如上所述,JMM说“不”。 Java内存模型的较新版本力求保证所有初始化的安全构建,以捕获此案例。许多非x86端口have already strengthened this是安全的。
琐事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 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读取后通过volatile
字段进行传递读取,则会在构造函数中观察由volatile写入的值,通常会发布安全发布规则。