不变性和重新sorting

评论接受的答案

这个问题产生的热量比我想象中的要多得多。 一个重要的结论,我从与并发利益邮件列表成员(即实际工作的人)的公共和私人讨论中吸取了一些重要的结论:

如果你可以find一个顺序一致的重新sorting,不会中断任何线程间发生的关系,这是一个有效的重新sorting(即符合程序顺序规则和因果关系要求)。

约翰·文特(John Vint)在他的回答中提供了这一点。


原来的问题

下面的代码(Java Concurrency in Practice列表16.3)不是线程安全的,原因很明显:

public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { if (resource == null) resource = new Resource(); // unsafe publication return resource; } } 

然而,在几页之后的16.3节中,他们指出:

如果 Resource不可变, UnsafeLazyInitialization实际上是安全的。

我不明白这个说法:

  • 如果Resource是不可变的,那么任何观察resourcevariables的线程将会看到它为null或完全构造(由于Java Memory模型提供的最终字段的强有力的保证)
  • 然而,没有什么可以防止指令重新sorting:特别是两个resource读取可以重新sorting(有一个读取的ifreturn )。 所以一个线程可以在if条件中看到一个非null的resource ,但返回一个空引用(*)。

我认为即使Resource是不可变的, UnsafeLazyInitialization.getInstance()也可以返回null。 是这样,为什么(或为什么不)?

注:我期望有一个有争议的答案,而不是单纯的是或否的陈述。


(*)为了更好地理解我关于重新sorting的观点,作为JLS第17章并发性之一的作者之一的Jeremy Manson 发表的这篇博客文章解释了如何通过良性数据竞赛安全地发布String的哈希码以及如何删除使用局部variables可能会导致哈希码错误地返回0,因为可能的重新sorting非常类似于我上面描述的:

我在这里所做的是添加一个额外的阅读:在返回之前散列的第二次阅读。 虽然听起来很奇怪,但也不太可能发生,第一次读取可以返回正确计算的散列值,第二次读取可以返回0! 这在内存模型下是允许的,因为模型允许对操作进行大量的重新sorting。 第二次读取实际上可以在你的代码中被移动,这样你的处理器就可以在第一次之前执行它!

我想你在这里的困惑是作者所说的安全出版的意思。 他指的是一个非null资源的安全发布,但你似乎得到了这一点。

你的问题是有趣的 – 是否有可能返回一个空的caching值的资源?

是。

编译器可以像这样对操作进行重新sorting

 public static Resource getInstance(){ Resource reordered = resource; if(resource != null){ return reordered; } return (resource = new Resource()); } 

这不违反顺序一致性的规则,但可以返回一个空值。

不pipe这是否是最好的实施scheme,都有争议,但是没有规定来防止这种重新sorting。

在将JLS规则应用到这个例子之后,我得出了getInstance可以肯定返回null的结论。 特别是JLS 17.4 :

内存模型决定了程序中每个点可以读取的值。 隔离每个线程的行为必须遵循该线程的语义, 除了每次读取的值由内存模型决定

很显然, 在没有同步的情况下, null是该方法的合法结果,因为这两个读取中的每一个都可以观察到任何东西。


certificate

读取和写入的分解

该程序可以分解如下(以清楚地看到读取和写入):

  Some Thread --------------------------------------------------------------------- 10: resource = null; //default value //write ===================================================================== Thread 1 | Thread 2 ----------------------------------+---------------------------------- 11: a = resource; | 21: x = resource; //read 12: if (a == null) | 22: if (x == null) 13: resource = new Resource(); | 23: resource = new Resource(); //write 14: b = resource; | 24: y = resource; //read 15: return b; | 25: return y; 

JLS说的

JLS 17.4.5给出了允许读取的读取规则:

我们说,如果在执行轨迹的发生部分顺序之前,variablesv的读取r被允许观察对v的写入w:

  • r不是在w之前sorting的(也就是说,hb(r,w))和
  • (v,w')和hb(w',r))写入w'(即不写v)。

规则的应用

在我们的例子中,让我们假设线程1看到null并正确地初始化resource 。 在线程2中,一个无效的执行将是21观察23(由于程序顺序) – 但是任何其他写入(10和13)都可以通过读取来观察:

  • 10发生在所有行动之前,所以在10之前没有读取命令
  • 21和24与13没有关系
  • 13没有发生 – 23之前(两者之间没有HB关系)

因此21和24(我们的2个读数)都被允许观察10(空)或13(非空)。

执行path返回null

特别是,假设线程1在第11行看到一个空值并初始化第13行的resource ,线程2可以合法执行,如下所示:

  • 24: y = null (读取写入10)
  • 21: x = non null (读取写入13)
  • 22: false
  • 25: return y

注意:为了澄清, 这并不意味着T2看到非空,随后看到空 (这将违反因果关系要求) – 这意味着从执行的angular度来看,两个读取已经被重新sorting,第二个在第一个之前被提交一个 – 但是看起来后面的写作看起来好像之前的写作是根据最初的程序顺序看到的。

2月10日更新

回到代码, 有效的重新sorting是:

 Resource tmp = resource; // null here if (resource != null) { // resource not null here resource = tmp = new Resource(); } return tmp; // returns null 

而且,因为代码是顺序一致的(如果由单个线程执行,它将始终具有与原始代码相同的行为),它表明因果关系要求得到满足(有一个有效的执行产生结果)。


在发布并发兴趣列表之后,我收到了一些关于重新sorting合法性的信息,这些信息确认null是一个合法的结果:

  • 由于单线程执行不能区分这种差异,所以转换是绝对合法的。 [注意]转换似乎不明智 – 编译器没有理由这么做。 但是,如果周围的代码数量较多,或者编译器优化“bug”,可能会发生。
  • 关于线程内部sorting和程序顺序的说法是让我质疑事物的有效性,但最终JMM涉及到被执行的字节码。 这个转换可以通过javac编译器完成,在这种情况下,null将是完全有效的。 并没有规则如何javac必须从Java源代码转换为Java字节码…

更新Feb10

我确信我们应该分开两个阶段: 编译执行

我认为是否允许返回null的决定因素是字节码是什么 。 我做了3个例子:

例1:

原始的源代码,直接翻译成字节码:

 if (resource == null) resource = new Resource(); // unsafe publication return resource; 

字节码:

 public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: ifnonnull 16 6: new #22; //class Resource 9: dup 10: invokespecial #24; //Method Resource."<init>":()V 13: putstatic #20; //Field resource:LResource; 16: getstatic #20; //Field resource:LResource; 19: areturn 

这是最有趣的情况,因为有2个read (行#0和行#16),并且有1个write (行#13)。 我声称这是不可能重新sorting ,但让我们来看看下面。

例2

“编译器优化”代码,可以从字面上重新转换为java,如下所示:

 Resource read = resource; if (resource==null) read = resource = new Resource(); return read; 

这个字节码(实际上我是通过编译上面的代码片断来完成的):

 public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: astore_0 4: getstatic #20; //Field resource:LResource; 7: ifnonnull 22 10: new #22; //class Resource 13: dup 14: invokespecial #24; //Method Resource."<init>":()V 17: dup 18: putstatic #20; //Field resource:LResource; 21: astore_0 22: aload_0 23: areturn 

很显然, 如果编译器“优化”了 ,并且像上面这样的字节代码被生成,那么可能会发生空读(例如,我指的是Jeremy Manson的博客 )

看到a = b = c是如何工作也是很有趣的:对新实例的引用(行#14)被复制 (行#17),并且存储相同的引用,则首先对b (资源, #18))然后到a (阅读,(行#21))。

例3

让我们做一个更轻微的修改:只读取一次resource ! 如果编译器开始优化(和使用寄存器,如其他人提到的那样), 这比上面的优化更好 ,因为这里的第4行是一个“寄存器访问”,而不是比较昂贵的例子2中的“静态访问”。

 Resource read = resource; if (read == null) // reading the local variable, not the static field read = resource = new Resource(); return read; 

示例3的字节码(也是通过字面编译上述内容创build的):

 public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: astore_0 4: aload_0 5: ifnonnull 20 8: new #22; //class Resource 11: dup 12: invokespecial #24; //Method Resource."<init>":()V 15: dup 16: putstatic #20; //Field resource:LResource; 19: astore_0 20: aload_0 21: areturn 

也很容易看出,从字节码中得到null不可能的,因为它的构造方式与String.hashcode()相同,只有一次读取resource的静态variables。

现在让我们来看看例1

 0: getstatic #20; //Field resource:LResource; 3: ifnonnull 16 6: new #22; //class Resource 9: dup 10: invokespecial #24; //Method Resource."<init>":()V 13: putstatic #20; //Field resource:LResource; 16: getstatic #20; //Field resource:LResource; 19: areturn 

你可以看到Line#16(读取variable#20作为返回值)大多数观察到了来自Line#13的写入(从构造函数中分配variable#20 ),所以把它放在任何执行顺序的地方是非法的行#13被执行 。 所以, 重新sorting是不可能的

对于一个JVM,可以构造(并利用)一个分支(使用某些额外的条件)绕过行#13写:条件是从variable#20读取不能为空

所以,在任何情况下, 例1都可能返回null。

结论:

看上面的例子, 例1中的字节码不会产生null 。 像例2中的优化字节码将PROCUDE为null ,但有一个更好的优化例3 ,它不会产生null

因为我们不能为所有编译器的所有可能的优化做好准备,所以我们可以说在某些情况下是可能的, 其他一些情况下不可能return null ,这一切都取决于字节码。 此外,我们已经表明, 这两种情况都至less有一个例子


老推理 :引用Assylias的例子:主要问题是:VM是否对11和14读取次序是有效的(关于所有规格,JMM,JLS),那么14会在11之前发生?

如果可能发生,那么独立的Thread2可以用23来写资源,所以14可以读取null 。 我说这是不可能的

事实上,因为有可能写13,这不是一个有效的执行顺序 。 虚拟机可以优化执行顺序,这样排除了不执行的分支(只剩下2个读取,不写入),但是为了做出这个决定, 它必须执行第一次读取(11),并且它必须读取非空值 ,所以14读不能先于11读 。 所以,不可能返回null


不变性

关于不变性,我认为这个说法是正确的:

如果Resource不可变,UnsafeLazyInitialization实际上是安全的。

但是,如果构造函数不可预知,可能会出现有趣的结果。 想象一下这样的构造函数:

 public class Resource { public final double foo; public Resource() { this.foo = Math.random(); } } 

如果我们有Thread s,可能会导致2线程将收到一个不同行为的对象。 所以,完整的陈述应该是这样的:

如果资源不可变且初始化一致,则UnsafeLazyInitialization实际上是安全的。

通过一致的我的意思是调用Resource的构造函数两次,我们将收到两个对象行为完全相同的方式(调用相同的方法在相同的顺序都将产生相同的结果)。

基本上有两个问题你问:

1.由于重新sorting, getInstance()方法是否可以返回null

(我认为这是你真的以后,所以我会尽量先回答它)

即使我认为deviseJava允许这是完全疯狂的,似乎你实际上是正确的getInstance()可以返回null。

您的示例代码:

 if (resource == null) resource = new Resource(); // unsafe publication return resource; 

逻辑上与链接到的博客文章中的示例完全相同:

 if (hash == 0) { // calculate local variable h to be non-zero hash = h; } return hash; 

杰里米·曼森然后描述他的代码可以返回0由于重新sorting。 起初,我不相信,因为我认为以下“发生之前” – 逻辑必须坚持:

  "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; 

这在单线程环境中的行为与原始代码完全相同,但在multithreading环境中可能导致以下执行顺序:

 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!!! 

现在,从优化的angular度来看,这样做对我来说没有任何意义,因为这些东西的全部意义在于将多次读取减less到相同的位置,在这种情况下,编译器不会生成if (read==null)而不是这个问题。 所以,正如Jeremy在他的博客中指出的那样,这种情况发生的可能性不大。 但是,似乎纯粹从语言规则的angular度来看,事实上是允许的。

这个例子实际上覆盖了JLS:

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

Table 17.4. Surprising results caused by forward substitutionr2r4r5的值之间观察到的效果Table 17.4. Surprising results caused by forward substitution Table 17.4. Surprising results caused by forward substitution相当于上面例子中的read = resourceif (resource==null)return resource发生的情况。

另外:为什么我将博客文章作为答案的最终来源? 因为写这个的人也是编写JLS第17章并发的人! 所以,他最好是对的! 🙂

2.使Resource不可变让getInstance()方法是线程安全的?

鉴于潜在的null结果,可以独立于Resource是否可变而发生,这个问题的直接简单答案是: (不严格)

如果我们忽略这种极不可能但可能的情况,答案是: 取决于

代码中显而易见的线程问题是它可能导致以下执行顺序(不需要任何重新sorting):

 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 

所以,非线程安全来自于这样一个事实,即你可能从函数中得到两个不同的对象(即使没有重新sorting它们都不会为null )。

现在,这本书可能要说的是:

像String和Integers这样的Java不可变对象试图避免为相同的内容创build多个对象。 所以,如果你在一个地方有"hello" ,而在另一个地方有"hello" ,Java会给你同样的确切的对象引用。 同样,如果你有一个new Integer(5)在一个点和new Integer(5)在另一个。 如果new Resource()也是这种情况,那么您将得到相同的引用,上例中的object 1object 2将是完全相同的对象。 这确实会导致一个有效的线程安全函数(忽略重新sorting问题)。

但是,如果你自己实现Resource ,我不相信甚至有办法让构造函数返回一个以前创build的对象的引用,而不是创build一个新的。 所以,你不应该让object 1object 2成为完全相同的对象。 但是,假设你用相同的参数调用构造函数(在这两种情况下都是没有的),那么即使你创build的对象不是同一个确切的对象,他们可能会出于所有的意图和目的如果他们是,也有效地使代码线程安全。

虽然这并不一定是这种情况。 例如,想象一下Date的不可变版本。 默认的构造函数Date()使用当前系统时间作为date值。 所以,即使对象是不可变的,并且使用相同的参数调用构造函数,调用它两次可能不会导致等效的对象。 因此, getInstance()方法不是线程安全的。

所以,作为一般性陈述,我相信你从这本书引用的这一行是错误的(至less在这里是脱离了语境)。

附加Re:重新sorting

我发现resource==new Resource()例子有点太简单了,以至于不能理解为什么允许Java这样的重新sorting是有意义的。 所以,让我看看我是否可以拿出一些实际上有助于优化的东西:

 System.out.println("Found contact:"); System.out.println(firstname + " " + lastname); if (firstname==null) firstname = ""; if (lastname ==null) lastname = ""; return firstname + " " + lastname; 

在这种情况下,最有可能的情况是, ifs两个ifs产生false ,那么执行两次昂贵的String concatenation firstname + " " + lastname是非最优的,一次是debugging消息,一次是返回。 所以,在这里重新排列代码来做下面的事情是有道理的:

 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; 

随着例子变得越来越复杂,当你开始考虑编译器跟踪它使用的处理器寄存器中已经加载/计算的内容,并且智能地跳过已经存在的结果的重新计算时,这种影响可能实际上变得越来越可能发生。 所以,尽pipe我昨天晚上睡觉的时候从来没有想过我会这样说,但是现在我确实相信,这可能是一个需要/很好的决定,以确保代码优化能够做到最好神奇的魔法。 但是它仍然让我觉得非常危险,因为我不认为很多人都知道这一点,即使它们是这样,如何正确地编写代码而不是同步所有东西(这将会消失很多时候从更灵活的优化中获得任何性能优势)。

我想如果你不允许这种重新sorting,任何caching和重用一系列处理步骤的中间结果将是非法的,因此可以取消最强大的编译器优化之一。

现在这是一个非常长的后台线程,仍然给这个问题讨论了重新sorting和并发的许多有趣的工作,我最近也参与到这里。

一时之间,如果我们不涉及并发,multithreading情况下的行为和有效的重新sorting。
“JVM可以在单线程上下文中使用caching值后写操作”。 我想不是。 考虑到有一个写入操作,如果条件可以caching进来播放。
所以回到这个问题,不可变性确保对象在引用被访问或发布之前完全或正确地创build,所以不变性肯定有帮助。 但是在创build对象之后有一个写操作。 那么第二次读取的数据是否可以在预写的时候在同一个线程中caching呢? 一个线程可能不知道其他线程中的写入(因为线程之间不需要立即可见)。 所以不会返回一个虚假的null(即创build对象之后)无效的可能性。 (有问题的代码打破单身人士,但我们不打扰在这里)

一旦它为非null就不会将引用设置为null 。 在另一个线程将其设置为非null之后,线程可能会看到null ,但我不知道如何反向是可能的。

我不确定指令重新sorting是否是这里的一个因素,但是两条线程的交叉指令是。 if分支不能以某种方式被重新sorting以在其条件被评估之前执行。

I'm sorry if I'm wrong (because I'm not native-English speaker), but it seems to me, that mentioned statement:

UnsafeLazyInitialization is actually safe if Resource is immutable.

is torn out of the context. This statement is truly regarding to use initialization safety :

The guarantee of initialization safety allows properly constructed immutable objects to be safely shared across threads without synchronization

Initialization safety guarantees that for properly constructed objects, all threads will see the correct values of final fields that were set by the constructor

After reading through the post you linked more carefully, you are correct, the example you posted could conceivably (under the current memory model) return null. The relevant example is way down in the comments of the post, but effectively, the runtime can do this:

 public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { Resource tmp = resource; if (resource == null) tmp = resource = new Resource(); // unsafe publication return tmp; } } 

This obeys the constraints for a single-thread, but could result in a null return value if multiple threads are calling the method (the first assignment to tmp gets a null value, the if block sees a non-null value, tmp gets returned as null).

In order to make this "safely" unsafe (assuming Resource is immutable), you have to explicitly read resource only once (similar to how you should treat a shared volatile variable:

 public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { Resource cur = resource; if (cur == null) { cur = new Resource(); resource = cur; } return cur; } } 

It is indeed safe is UnsafeLazyInitialization.resource is immutable, ie the field is declared as final:

 private static final Resource resource = new Resource(); 

It might also be considered as thread-safe if the Resource class itself is immutable and does not matter which instance you are using. In that case two calls could return different instances of Resource without issue apart from an increased memory consumption depending on the number of threads calling getInstance() at the same time).

It seems far-fetched though and I believe there is a typo, real sentence should be

UnsafeLazyInitialization is actually safe if * r *esource is immutable.

UnsafeLazyInitialization.getInstance() can never return null .

I'll use @assylias's table.

  Some Thread --------------------------------------------------------------------- 10: resource = null; //default value //write ===================================================================== Thread 1 | Thread 2 ----------------------------------+---------------------------------- 11: a = resource; | 21: x = resource; //read 12: if (a == null) | 22: if (x == null) 13: resource = new Resource(); | 23: resource = new Resource(); //write 14: b = resource; | 24: y = resource; //read 15: return b; | 25: return y; 

I'll use the line numbers for Thread 1. Thread 1 sees the write on 10 before the read on 11, and the read on line 11 before the read on 14. These are intra-thread happens-before relationships and don't say anything about Thread 2. The read on line 14 returns a value defined by the JMM. Depending on the timing, it may be the Resource created on line 13, or it may be any value written by Thread 2. But that write has to happen-after the read on line 11. There is only one such write, the unsafe publish on line 23. The write to null on line 10 is not in scope because it happened before line 11 due to intra -thread ordering.

It doesn't matter if Resource is immutable or not. Most of the discussion so far has focused on inter-thread action where immutability would be relevant, but the reordering that would allow this method to return null is forbidden by intra -thread rules. The relevant section of the spec is JLS 17.4.7 .

For each thread t, the actions performed by t in A are the same as would be generated by that thread in program-order in isolation, with each write w writing the value V(w), given that each read r sees the value V(W(r)). Values seen by each read are determined by the memory model. The program order given must reflect the program order in which the actions would be performed according to the intra-thread semantics of P.

This basically means that while reads and writes may be reordered, reads and writes to the same variable have to appear like they happen in order to the Thread that executes the reads and writes.

There's only a single write of null (on line 10). Either Thread can see its own copy of resource or the other Thread's, but it cannot see the earlier write to null after it reads either Resource.

As a side note, the initialization to null takes place in a separate thread. The section on Safe Publication in JCIP states:

Static initializers are executed by the JVM at class initialization time; because of internal synchronization in the JVM, this mechanism is guaranteed to safely publish any objects initialized in this way [JLS 12.4.2] .

It may be worth trying to write a test that gets UnsafeLazyInitialization.getInstance() to return null, and that gets some of the proposed equivalent rewrites to return null. You'll see that they're not truly equivalent.

编辑

Here's an example that separates reads and writes for clarity. Let's say there's a public static variable object.

 public static Object object = new Integer(0); 

Thread 1 writes to that object:

 object = new Integer(1); object = new Integer(2); object = new Integer(3); 

Thread 2 reads that object:

 System.out.println(object); System.out.println(object); System.out.println(object); 

Without any form of synchronization providing inter-thread happens-before relationships, Thread 2 can print out lots of different things.

 1, 2, 3 0, 0, 0 3, 3, 3 1, 1, 3 etc. 

But it cannot print out a decreasing sequence like 3, 2, 1. The intra-thread semantics specified in 17.4.7 severely limit reordering here. If instead of using object three times we changed the example to use three separate static variables, many more outputs would be possible because there would be no restrictions on reordering.