说,我们有代码:
Test t = new Test();
编译成字节代码实际上是三个步骤:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem
我想知道,如果构造(mem)非常慢,JIT会在步骤2中等待,直到mem完全构建?
那怎么能保证mem在使用前完全构造(单线程)?
那么为什么双重检查锁定(参见下面的代码,这个article)会失败?
class DB {
private DB(){}
private static DB instance;
public static DB getInstance() {
// First check
if(instance == null ){
synchronized(DB.class){
// Second check
if(instance == null) {
instance = new Instance();
}
}
}
return instance;
}
}
我提到的文章指出,上面的代码将返回一个尚未完全构建的实例。
答案 0 :(得分:4)
很久以前检查this answer I gave here on StackOverflow,了解DCL失败的原因以及解决方法。
问题不在于同步/异步。这个问题叫做重新排序。
JVM规范定义了一种称为发生在之前的关系。在单个线程内部,如果语句S1出现在语句S2之前,那么 S1发生在S2 之前,也就是说,S1对内存的任何修改都是可见的。请注意,它并未说明必须在S2之前执行语句S1。它只是说事情应该看起来像 S1在S2之前执行。例如,请考虑以下代码:
int x = 0;
int y = 0;
int z = 0;
x++;
y++;
z++;
z += x + y;
System.out.println(z);
这里,JVM执行三个增量语句的顺序无关紧要。唯一的保证是,当运行z += x + y
时,x,y和z的值必须全部为1.实际上,如果重新排序没有违反,则实际上允许JVM重新排序语句发生在关系之前。这样做的原因是,有时一点点重新排序可以优化您的代码,并且您可以获得更好的性能。
缺点是允许JVM以一种在使用多个线程时可能导致非常奇怪的结果的方式对事物进行重新排序。例如:
class Broken {
private int value;
private boolean initialized = false;
public void init() {
value = 5;
initialized = true;
}
public boolean isInitialized() { return initialized; }
public int getValue() { return value; }
}
假设线程正在执行此代码:
while (!broken.isInitialized()) {
Thread.sleep(1); // patiently wait...
}
System.out.println(broken.getValue());
假设现在,另一个线程在同一个Broken
实例上执行了
broken.init();
允许JVM对init()
方法中的代码进行重新排序,首先运行initialized = true
,然后再将value
设置为5.如果发生这种情况,则第一个线程,等待初始化的那个,可能会打印0!要解决此问题,请为这两种方法添加synchronized
,或将volatile
添加到initialized
字段。
回到DCL,单例的初始化可能以不同的顺序执行。例如:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem
可能会成为:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. t = mem : point t to the mem
3. construct(mem) : construct the class Test
因为,对于单个线程,两个块都是完全等效的。也就是说,对于单线程应用程序来说,这种单例初始化对于完全安全是绝对安全的。但是,对于多个线程,一个线程可能会获得部分初始化对象的引用!
为了确保在使用多个线程时语句之间的发生在之间的关系,您有两种可能:获取/释放锁和读/写volatile字段。要修复DCL,您必须声明包含单例volatile
的字段。这将确保单例(即运行其构造函数)的初始化发生在对包含单例的字段的任何读取之前。有关volatile如何修复DCL的详细解释,请检查我在此顶部链接的答案。
答案 1 :(得分:2)
我想知道,如果
construct(mem)
非常慢,JIT会在步骤2等待,直到mem
完全构建?
假设您正在讨论由JIT生成的代码 ...那么答案是代码不会必然在那时等待。这取决于在步骤3之后的内容。
那怎样才能保证
mem
在使用前完全构造(单线程)?
要求是该线程 1 中变量的观察值与语言的指定语义一致;即“程序顺序”。如果没有任何区别,JIT可以重新排序指令。具体来说,如果某些字段的内存写入被延迟,则无关紧要...如果该线程不需要从内存中读取这些变量的值。 (代码可能根本不需要读取它们,它可能从寄存器中读取它们,或者它可以从1级或2级缓存中获取它们。)
因此,对“如何确保它”的简短回答是,它通过以满足实际语言要求的顺序发出指令来实现它...而不是更严格的语义你假定。
我将你的问题的第二部分(关于DCL实施)视为没有实际意义。
1 - 这仅适用于该线程。 JLS声明对其他线程的一致性没有这样的要求......除非在写和后续读事件之间存在“先发生”关系。