这道题答案不是很重要,思路比较重要,上面有的人说的答案也对,但是不精确(有时候不精确不会产生大的问题,有时候就会,下面会解释
这个观点),所以LZ最好亲自试试下面的过程。另外,本题涉及几个知识点。我详细解释下。
1.如果你是想在JAVA代码级别解释这个问题,那么你是在浪费时间。这个问题必须到JVM生成的代码级别讨论(很多问题都是这个样子,在
JAVA代码级别讨论不仅浪费时间,而且没有意义,记得有人跟我说过一句话:在你所处理的层面,问题根本还没有浮现(非编程问题))。
2.
[code="java"]
public class TestJVM {
public static void main(String[] args)
{
TestJVM abc = new TestJVM();
}
}
[/code]
代码用javap -c 命令反编译TestJVM.class文件后(我建议你自己试试),生成
...
[code="java"]
public TestJVM();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #1; //class TestJVM
3: dup
4: invokespecial #16; //Method "":()V
7: astore_1
8: return
...
[/code]
解释这段代码是这道问题的第一步,建议你大概查阅下JVM规范,因为我也刚查了。
(1) new 的含义是创造一块内存,并且在堆栈上压入指向这块内存的引用。
(2) dup的含义是将栈顶复制,并压入栈。(所以现在有了两个指向刚才分配内存的引用)
(3) invokespecial意思是将分配的内存中初始化对象。
(4) astore_1是将栈顶压入本地变量。
(这段过程,我建议你自己多画几遍,体会下JVM"面向堆栈"的概念,JVM规范第一章最好看看)
3.上面的四个步骤(绝对的物理过程),其实就是三件事(体会一下原子语句的含义):
a.给实例分配内存。
b.初始化构造器
c.将引用指向分配的内存空间(注意到这步引用就非null了)。
一般来说,我们期望执行的步骤是a->b->c,然而,由于JVM乱序执行的特性(自己查查这句话在哪,别轻易相信别人,虽然有时候文档也是会
骗人的-!-),可能执行的顺序是a->c->b。当a->c->b这样执行时候,假如刚执行完c,这样线程2访问这个引用,发现引用不为空,他就对相
应的内存做操作,这样就会发生错误,这种错误想必不容易发现(那是不是不容易发生?取决于具体的应用环境。)。
4.问题的关键用一句话来概括,就是这个意思:if(instance==null),如果instance !=null,那么instance就真的准备好了么?
所以,最原始的写法虽然慢,但是不会产生这种问题,因为原始写法把判断是否等于null的语句,也给锁起来了。只有得到锁,才有资格判断
。
5.上面的几条,你也许看了第四条,或者大概明白前几条,你的问题就能解答了。不精确的了解似乎也能回答,但是,有好多误解就产生了。
比如,有人说,加了valatile类型修饰(JVM1.5以后)符可以将LZ的写法变对,如private volatile static Singleton instance = null;
其实这是不对的,valatile(LZ想想为什么valatile影响效率?理解下寄存器和内存的效率差别)无非说的就是线程是不能保留共享对象的本地
拷贝(正常情况线程是可以保留的),那是不是每次去内存中取,就能保证单例对象的正常初始化呢?很明显,这完全是两个问题。
6.很多细节问题(编程方面),你都得查查英文文档,得自己写试试,中文大家说的话都非常像(因为都是同一本书里面说的,再加上第一个
人的翻译水平不咋样),很多误解就此产生。