您要问的主要有两个问题:
1.getInstance()
方法null
会因为重排而返回吗?
(我认为这是您真正追求的,所以我会先尝试回答)
尽管我认为设计 Java 来实现这一点是完全疯狂的,但实际上可以返回 null似乎是正确的。getInstance()
您的示例代码:
if (resource == null)
resource = new Resource(); // unsafe publication
return resource;
在逻辑上与您链接到的博客文章中的示例 100% 相同:
if (hash == 0) {
// calculate local variable h to be non-zero
hash = h;
}
return hash;
Jeremy Manson 然后描述了他的代码可以由于重新排序而返回 0。起初,我不相信,因为我认为以下“发生在之前”的逻辑必须成立:
"if (resource == null)" happens before "resource = new Resource();"
and
"resource = new Resource();" happens before "return resource;"
therefore
"if (resource == null)" happens before "return resource;", preventing null
但是 Jeremy 在他的博客文章的评论中给出了以下示例,说明编译器如何有效地重写此代码:
read = resource;
if (resource==null)
read = resource = new Resource();
return read;
这在单线程环境中的行为与原始代码完全相同,但在多线程环境中可能会导致以下执行顺序:
Thread 1 Thread 2
------------------------------- -------------------------------------------------
read = resource; // null
read = resource; // null
if (resource==null) // true
read = resource = new Resource(); // non-null
return read; // non-null
if (resource==null) // FALSE!!!
return read; // NULL!!!
现在,从优化的角度来看,这样做对我来说没有任何意义,因为这些事情的全部意义在于减少对同一位置的多次读取,在这种情况下,编译器不会这样做是没有意义的而是生成if (read==null)
,防止出现问题。因此,正如 Jeremy 在他的博客中指出的那样,这很可能不太可能发生。但似乎,纯粹从语言规则的角度来看,它实际上是允许的。
这个例子实际上在 JLS 中有介绍:
http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4
r2
在、r4
和r5
in的值之间观察到的效果等同于上面示例中的、 和 theTable 17.4. Surprising results caused by forward substitution
可能发生的情况。read = resource
if (resource==null)
return resource
旁白:为什么我将博客文章作为答案的最终来源?因为写它的人,也是写了 JLS 第 17 章关于并发的人!所以,他最好是对的!:)
2. 使方法Resource
不可变会使getInstance()
方法线程安全吗?
考虑到潜在的null
结果,它可以独立于是否Resource
可变而发生,这个问题的直接简单答案是:不(不严格)
但是,如果我们忽略这种极不可能但可能的情况,答案是:取决于。
代码的明显线程问题是它可能导致以下执行顺序(无需任何重新排序):
Thread 1 Thread 2
---------------------------------------- ----------------------------------------
if (resource==null) // true;
if (resource==null) // true
resource=new Resource(); // object 1
return resource; // object 1
resource=new Resource(); // object 2
return resource; // object 2
因此,非线程安全性来自这样一个事实,即您可能会从函数中返回两个不同的对象(即使不重新排序它们都不会是null
)。
现在,这本书可能想说的是:
Java 不可变对象(如字符串和整数)试图避免为相同的内容创建多个对象。因此,如果您"hello"
在一个地方和"hello"
另一个地方都有,Java 将为您提供相同的确切对象引用。同样,如果您new Integer(5)
在一个地方和new Integer(5)
另一个地方都有。如果也是这种情况new Resource()
,您将获得相同的引用,object 1
并且object 2
在上面的示例中将是完全相同的对象。这确实会导致有效的线程安全功能(忽略重新排序问题)。
但是,如果您Resource
自己实现,我认为甚至没有办法让构造函数返回对先前创建的对象的引用,而不是创建一个新对象。所以,你不可能制造object 1
和object 2
成为完全相同的对象。但是,鉴于您正在使用相同的参数调用构造函数(两种情况下都没有),即使您创建的对象不是同一个确切的对象,它们也可能出于所有意图和目的,表现为如果它们是,也有效地使代码线程安全。
不过,情况不一定如此。例如,想象一个不可变的版本Date
。默认构造函数Date()
使用当前系统时间作为日期值。因此,即使对象是不可变的并且使用相同的参数调用构造函数,调用它两次也可能不会产生等效对象。因此该getInstance()
方法不是线程安全的。
所以,作为一般性声明,我相信你从书中引用的那句话是完全错误的(至少在这里断章取义)。
补充回复:重新排序
我发现这个resource==new Resource()
例子有点过于简单,无法帮助我理解为什么允许通过 Java 重新排序是有意义的。所以让我看看我是否能想出一些真正有助于优化的东西:
System.out.println("Found contact:");
System.out.println(firstname + " " + lastname);
if (firstname==null) firstname = "";
if (lastname ==null) lastname = "";
return firstname + " " + lastname;
ifs
在这里,在两者都yield的最可能情况下,false
执行两次昂贵的字符串连接是非最佳的firstname + " " + lastname
,一次用于调试消息,一次用于返回。因此,在这里重新排序代码以执行以下操作确实是有意义的:
System.out.println("Found contact:");
String contact = firstname + " " + lastname;
System.out.println(contact);
if ((firstname==null) || (lastname==null)) {
if (firstname==null) firstname = "";
if (lastname ==null) lastname = "";
contact = firstname + " " + lastname;
}
return contact;
随着示例变得越来越复杂,并且当您开始考虑编译器跟踪它使用的处理器寄存器中已经加载/计算的内容并智能地跳过重新计算已经存在的结果时,这种效果实际上可能变得越来越可能发生。所以,即使我从没想过我昨晚睡觉时会这么说,但我现在确实相信这可能是真正允许代码优化发挥最大作用的必要/好的决定令人印象深刻的魔术。但它仍然让我觉得很危险,因为我认为很多人都没有意识到这一点,即使他们知道,它
我猜如果你不允许这种重新排序,任何对一系列处理步骤的中间结果的缓存和重用都会变得非法,从而取消可能的最强大的编译器优化之一。