我一直很乐于深入研究多线程编程的细节,尽管阅读了多年的CPU内存一致性模型,无等待和无锁算法,Java内存模型,实践中的Java并发性等知识,但我始终很喜欢。等等-我仍然会创建多线程编程错误。 总是令人惊奇的谦卑经历,使我想起这个问题有多复杂。
如果您已经阅读过JMM,那么您可能会记得,他们加强的领域之一是保证构造函数完成后最终字段的可见性。 例如,
public class ClassA {public final String b;public ClassA(String b) {this.b = b;}
}
...
ClassA x = new ClassA("hello");
JMM指出,每个线程(甚至构成ClassA实例x的线程除外)都将
始终将 xb视为“ hello”,并且永远不会看到null值(参考字段的默认值)。
真是太好了! 这意味着我们可以通过将字段标记为final来创建不可变对象,并且任何构造的实例都可以自动在线程之间共享,而无需进行其他工作来保证内存可见性。 ! 不利的一面是,如果未将ClassA.b标记为final,那么您将没有此类保证。 其他线程可能会观察到xb ==空结果(如果未使用其他“安全发布”机制来获得可见性)
当他们创建新的JMM时,每个人最喜欢的JCP成员Doug Lea都创建了一个食谱,以帮助JVM开发人员实现新的内存模型规则。 如果阅读此内容,那么您将看到“规则”状态,即JIT编译器应在构造函数返回之前立即发出StoreStore内存屏障。 该StoreStore障碍是一种“内存围栏”。 如果在汇编指令中发出该信息,则意味着在对栅栏进行重新排序之前, 在栅栏之前出现的内存写之前,不能对内存进行任何写(存储)操作。 请注意,它没有说明读取内容,它们可以沿任一方向“跳”栅栏。
那么这是什么意思? 如果您考虑在调用构造函数时编译器的功能,那么很好:
String x = new ClassA("hello");get's broken down in to pseudo-code steps of:1. pointer_to_A = allocate memory for ClassA (mark word, class object pointer, one reference field for String b)
2. pointer_to_A.whatever class meta data = ...
3. pointer_to_A.b = address of "hello" string
4. emit a StoreStore memory barrier per the JMM
5. x = pointer_to_A
步骤4的StoreStore屏障可确保任何写入(例如类元数据和对字段b的写入)都不会在步骤5中对x进行重新排序。这可以确保x是否对任何其他线程可见-如果没有StoreStore内存屏障,那么可以重新排序步骤3和5 ,并且在写入xb和另一个cpu之前可能会出现对x的主内存写入操作内核可以观察到pointer_to_A.b为0(空),这将违反JMM。
好消息! 但是,如果您看一下该菜谱,就会发现一些有趣的事情:(1)很多人正在许多处理器体系结构上编写JVM! (2)x86上的所有*存储屏障都没有操作,除了StoreLoad屏障! 这意味着在x86上,上面的此StoreStore内存屏障为空操作,因此不会为此发出任何程序集。 它什么都不做! 这是因为x86的内存模型是强大的 “总存储排序”(TSO)。 X86确保观察到所有内存写入,就像它们都是以相同顺序进行的一样。 因此,由于TSO的缘故,写入5永远不会出现在任何其他线程的3之前,并且不需要发出存储屏障。 其他cpu体系结构的内存模型较弱,无法保证这种效果,因此需要StoreStore内存围墙。 请注意,较弱的内存模型虽然可能更难编程或较不直观,但通常要快得多,因为cpu可以对事物进行重新排序以更有效地使用缓存写入并减少缓存一致性工作。
显然,您应该继续遵循JMM编写正确的代码。 但是,这也意味着(不幸或幸运的是)如果您在x86上运行,忘记此操作不会导致错误……就像我在工作中所做的那样。
为了真正钻研这个家并确保没有食谱中可能没有描述的其他副作用,我按此处所述运行了x86程序集输出程序,并捕获了为ClassA调用构造函数的输出(最后一个在引用类型字段)和ClassB的构造函数,该类与ClassA相同,除了在类成员上没有final关键字之外。 x86程序集的输出是相同的 。 因此,从JIT角度来看,在x86(非钛合金,非arm等)上,final关键字没有任何影响。
如果您想知道汇编代码的外观,则如下所示。 请注意,没有任何锁定说明。 当Oracle的7u25 JRE发出x86 StoreLoad内存隔离栅时,它是通过发出锁addl $ 0x0,(%rsp)来完成的 ,它仅向堆栈指针添加零(无操作, 但由于其被锁定),因此具有完整的作用。栅栏(符合StoreLoad栅栏的条件)。 x86中有几种导致完全隔离的效果的方法,这些方法在OpenJDK邮件列表中进行了讨论。 他们观察到至少在nehelem intel上,锁添加0是最紧凑/有效的空间。
0x00007f152c020c60: mov %eax,-0x14000(%rsp)0x00007f152c020c67: push %rbp0x00007f152c020c68: sub $0x20,%rsp ;*synchronization entry; - com.argodata.match.profiling.FinalConstructorMain::callA@-1 (line 60)0x00007f152c020c6c: mov %rdx,(%rsp)0x00007f152c020c70: mov %esi,%ebp0x00007f152c020c72: mov 0x60(%r15),%rax0x00007f152c020c76: mov %rax,%r100x00007f152c020c79: add $0x18,%r100x00007f152c020c7d: cmp 0x70(%r15),%r100x00007f152c020c81: jae 0x00007f152c020cd60x00007f152c020c83: mov %r10,0x60(%r15)0x00007f152c020c87: prefetchnta 0xc0(%r10)0x00007f152c020c8f: mov $0x8356f3d0,%r11d ; {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}0x00007f152c020c95: mov 0xb0(%r11),%r100x00007f152c020c9c: mov %r10,(%rax)0x00007f152c020c9f: movl $0x8356f3d0,0x8(%rax) ; {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}0x00007f152c020ca6: mov %r12d,0x14(%rax) ;*new ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)0x00007f152c020caa: mov %ebp,0xc(%rax) ;*putfield a; - com.argodata.match.profiling.FinalConstructorMain$ClassA::@6 (line 17); - com.argodata.match.profiling.FinalConstructorMain::callA@6 (line 60)0x00007f152c020cad: mov (%rsp),%r100x00007f152c020cb1: mov %r10d,0x10(%rax) ;*new ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)0x00007f152c020cb5: mov %rax,%r100x00007f152c020cb8: shr $0x9,%r100x00007f152c020cbc: mov $0x7f152b765000,%r110x00007f152c020cc6: mov %r12b,(%r11,%r10,1) ;*synchronization entry; - com.argodata.match.profiling.FinalConstructorMain::callA@-1 (line 60)0x00007f152c020cca: add $0x20,%rsp0x00007f152c020cce: pop %rbp0x00007f152c020ccf: test %eax,0x9fb932b(%rip) # 0x00007f1535fda000; {poll_return}0x00007f152c020cd5: retq 0x00007f152c020cd6: mov $0x8356f3d0,%rsi ; {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}0x00007f152c020ce0: xchg %ax,%ax0x00007f152c020ce3: callq 0x00007f152bfc51e0 ; OopMap{[0]=Oop off=136};*new ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60); {runtime_call}0x00007f152c020ce8: jmp 0x00007f152c020caa ;*new; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)0x00007f152c020cea: mov %rax,%rsi0x00007f152c020ced: add $0x20,%rsp0x00007f152c020cf1: pop %rbp0x00007f152c020cf2: jmpq 0x00007f152bfc8920 ; {runtime_call}
翻译自: https://www.javacodegeeks.com/2013/11/java-final-fields-on-x86-a-no-op.html