您要问的主要有两个问题:
1. getInstance() 方法能否因为重新排序而返回 null?
(我认为这是您真正追求的,所以我会先尝试回答)
尽管我认为设计 Java 来实现这一点是完全疯狂的,但您似乎是实际上是正确的 getInstance() 可以返回 null。
您的示例代码:
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
Table 17.4. Surprising results caused by forward substitution 中 r2、r4 和 r5 的值之间观察到的效果等同于使用 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 都产生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;
随着示例变得越来越复杂,并且当您开始考虑编译器跟踪它使用的处理器寄存器中已经加载/计算的内容并智能地跳过重新计算已经存在的结果时,这种效果实际上可能会变得更加更有可能发生。所以,即使我从没想过我昨晚睡觉时会这么说,但我现在确实相信这可能是真正允许代码优化发挥最大作用的必要/好的决定令人印象深刻的魔术。但它仍然让我觉得很危险,因为我认为很多人都没有意识到这一点,即使他们知道,在不同步所有内容的情况下如何正确编写代码也是相当复杂的(这将消除通过更灵活的优化获得的任何性能优势都翻了很多倍)。
我猜如果你不允许这种重新排序,任何对一系列处理步骤的中间结果的缓存和重用都将成为非法,从而取消可能的最强大的编译器优化之一.