首先要解释一下什么是延迟加载,延迟加载就是等到真真使用的时候才去创建实例,不用时不要去创建。
从速度和反应时间角度来讲,非延迟加载(又称饿汉式)好;从资源利用效率上说,延迟加载(又称懒汉式)好。
下面看看几种常见的单例的设计方式:
第一种:非延迟加载单例类
- public class Singleton {
- private Singleton() {}
- private static final Singleton instance = new Singleton();
- public static Singleton getInstance() {
- return instance;
- }
- }
public class Singleton {
private Singleton() {}
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
第二种:同步延迟加载
- public class Singleton {
- private static Singleton instance = null;
- private Singleton() {}
- public static synchronized Singleton getInstance() {
- if (instance == null) {
- instance = new Singleton();
- }
- return instance;
- }
- }
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
第三种:双重检测同步延迟加载
为处理原版非延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为同步块外面的if (instance == null)可能看到已存在,但不完整的实例。JDK5.0以后版本若instance为volatile则可行:
- public class Singleton {
- private volatile static Singleton instance = null;
- private Singleton() {}
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {// 1
- if (instance == null) {// 2
- instance = new Singleton();// 3
- }
- }
- }
- return instance;
- }
- }
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {// 1
if (instance == null) {// 2
instance = new Singleton();// 3
}
}
}
return instance;
}
}
双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。
无序写入:
为解释该问题,需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。
什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:
1、线程 1 进入 getInstance() 方法。
2、由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。
3、线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。
4、线程 1 被线程 2 预占。
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
mem = allocate(); //为单例对象分配内存空间.
instance = mem; //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance); //为单例对象通过instance调用构造函数
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
如果真像这篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所说那样的话,1.2或以后的版本就不会有问题了,但这个规则是JMM的规范吗?谁能够确认一下。
确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,
初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.
但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
向主存储区复制赋值时,其它线程绝对不可能见到这个过程.而这个字段复制到主存区的过程,更不会有分配空间后
没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作
存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource
为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见.
另一篇详细分析文章:http://www.iteye.com/topic/260515
第四种:使用ThreadLocal修复双重检测
借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。
- public class Singleton {
- private static final ThreadLocal perThreadInstance = new ThreadLocal();
- private static Singleton singleton ;
- private Singleton() {}
- public static Singleton getInstance() {
- if (perThreadInstance.get() == null){
- // 每个线程第一次都会调用
- createInstance();
- }
- return singleton;
- }
- private static final void createInstance() {
- synchronized (Singleton.class) {
- if (singleton == null){
- singleton = new Singleton();
- }
- }
- perThreadInstance.set(perThreadInstance);
- }
- }
public class Singleton {
private static final ThreadLocal perThreadInstance = new ThreadLocal();
private static Singleton singleton ;
private Singleton() {}
public static Singleton getInstance() {
if (perThreadInstance.get() == null){
// 每个线程第一次都会调用
createInstance();
}
return singleton;
}
private static final void createInstance() {
synchronized (Singleton.class) {
if (singleton == null){
singleton = new Singleton();
}
}
perThreadInstance.set(perThreadInstance);
}
}
第五种:使用内部类实现延迟加载
为了做到真真的延迟加载,双重检测在Java中是行不通的,所以只能借助于另一类的类加载加延迟加载:
- public class Singleton {
- private Singleton() {}
- public static class Holder {
- // 这里的私有没有什么意义
- /* private */static Singleton instance = new Singleton();
- }
- public static Singleton getInstance() {
- // 外围类能直接访问内部类(不管是否是静态的)的私有变量
- return Holder.instance;
- }
- }
public class Singleton {
private Singleton() {}
public static class Holder {
// 这里的私有没有什么意义
/* private */static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
// 外围类能直接访问内部类(不管是否是静态的)的私有变量
return Holder.instance;
}
}
单例测试
下面是测试单例的框架,采用了类加载器与反射。
注,为了测试单便是否为真真的单例,我自己写了一个类加载器,且其父加载器设置为根加载器,这样确保Singleton由MyClassLoader加载,如果不设置为根加载器为父加载器,则默认为系统加载器,则Singleton会由系统加载器去加载,但这样我们无法卸载类加载器,如果加载Singleton的类加载器卸载不掉的话,那么第二次就不能重新加载Singleton的Class了,这样Class不能得加载则最终导致Singleton类中的静态变量重新初始化,这样就无法测试了。
下面测试类延迟加载的结果是可行的,同样也可用于其他单例的测试:
- public class Singleton {
- private Singleton() {}
- public static class Holder {
- // 这里的私有没有什么意义
- /* private */static Singleton instance = new Singleton();
- }
- public static Singleton getInstance() {
- // 外围类能直接访问内部类(不管是否是静态的)的私有变量
- return Holder.instance;
- }
- }
- class CreateThread extends Thread {
- Object singleton;
- ClassLoader cl;
- public CreateThread(ClassLoader cl) {
- this.cl = cl;
- }
- public void run() {
- Class c;
- try {
- c = cl.loadClass("Singleton");
- // 当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法
- Method m = c.getMethod("getInstance", new Class[] {});
- // 调用静态方法时,传递的第一个参数为class对象
- singleton = m.invoke(c, new Object[] {});
- c = null;
- cl = null;
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- class MyClassLoader extends ClassLoader {
- private String loadPath;
- MyClassLoader(ClassLoader cl) {
- super(cl);
- }
- public void setPath(String path) {
- this.loadPath = path;
- }
- protected Class findClass(String className) throws ClassNotFoundException {
- FileInputStream fis = null;
- byte[] data = null;
- ByteArrayOutputStream baos = null;
- try {
- fis = new FileInputStream(new File(loadPath
- + className.replaceAll("\\.", "\\\\") + ".class"));
- baos = new ByteArrayOutputStream();
- int tmpByte = 0;
- while ((tmpByte = fis.read()) != -1) {
- baos.write(tmpByte);
- }
- data = baos.toByteArray();
- } catch (IOException e) {
- throw new ClassNotFoundException("class is not found:" + className,
- e);
- } finally {
- try {
- if (fis != null) {
- fis.close();
- }
- if (fis != null) {
- baos.close();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return defineClass(className, data, 0, data.length);
- }
- }
- class SingleTest {
- public static void main(String[] args) throws Exception {
- while (true) {
- // 不能让系统加载器直接或间接的成为父加载器
- MyClassLoader loader = new MyClassLoader(null);
- loader
- .setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\");
- CreateThread ct1 = new CreateThread(loader);
- CreateThread ct2 = new CreateThread(loader);
- ct1.start();
- ct2.start();
- ct1.join();
- ct2.join();
- if (ct1.singleton != ct2.singleton) {
- System.out.println(ct1.singleton + " " + ct2.singleton);
- }
- // System.out.println(ct1.singleton + " " + ct2.singleton);
- ct1.singleton = null;
- ct2.singleton = null;
- Thread.yield();
- }
- }
- }
public class Singleton {
private Singleton() {}
public static class Holder {
// 这里的私有没有什么意义
/* private */static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
// 外围类能直接访问内部类(不管是否是静态的)的私有变量
return Holder.instance;
}
}
class CreateThread extends Thread {
Object singleton;
ClassLoader cl;
public CreateThread(ClassLoader cl) {
this.cl = cl;
}
public void run() {
Class c;
try {
c = cl.loadClass("Singleton");
// 当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法
Method m = c.getMethod("getInstance", new Class[] {});
// 调用静态方法时,传递的第一个参数为class对象
singleton = m.invoke(c, new Object[] {});
c = null;
cl = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyClassLoader extends ClassLoader {
private String loadPath;
MyClassLoader(ClassLoader cl) {
super(cl);
}
public void setPath(String path) {
this.loadPath = path;
}
protected Class findClass(String className) throws ClassNotFoundException {
FileInputStream fis = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
fis = new FileInputStream(new File(loadPath
+ className.replaceAll("\\.", "\\\\") + ".class"));
baos = new ByteArrayOutputStream();
int tmpByte = 0;
while ((tmpByte = fis.read()) != -1) {
baos.write(tmpByte);
}
data = baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("class is not found:" + className,
e);
} finally {
try {
if (fis != null) {
fis.close();
}
if (fis != null) {
baos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return defineClass(className, data, 0, data.length);
}
}
class SingleTest {
public static void main(String[] args) throws Exception {
while (true) {
// 不能让系统加载器直接或间接的成为父加载器
MyClassLoader loader = new MyClassLoader(null);
loader
.setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\");
CreateThread ct1 = new CreateThread(loader);
CreateThread ct2 = new CreateThread(loader);
ct1.start();
ct2.start();
ct1.join();
ct2.join();
if (ct1.singleton != ct2.singleton) {
System.out.println(ct1.singleton + " " + ct2.singleton);
}
// System.out.println(ct1.singleton + " " + ct2.singleton);
ct1.singleton = null;
ct2.singleton = null;
Thread.yield();
}
}
}
评论
非常经典而详尽的研究。所以说最终的问题不是所谓构造函数执行到一半这个对象不完整的问题,而是因为DCL逃避了锁机制,所以会发生看不到对象中未被同步属性的最新值,发生错误。其实大抵和我贴的那个的下面一段讲的是一个意思。这篇讲得更精确更清晰。
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
在2001年,双检锁就已经安全了的结论不是那么容易下的吧。
请参考这篇发布于2004年的文章,里面提及了双检查锁存在的问题及JMM的修复。
http://www.ibm.com/developerworks/cn/java/j-jtp02244/
貌似我那篇文章是2006年的~~ 2004年的文章就更老了。。。我想应该不会有多少人还在使用非常古老的jvm吧。。。毕竟JDK5.0与先前的区别是非常大的。至少我看到的关于懒汉式单例的书里只提到过去的java使用懒汉式是不安全的,没有提到现在不能这么使用~~可能也是我的不严谨,不过我想这个问题的应用空间应该也不是很大吧,因为现在很少有需要使用单例的情况了,一般都是交给容器管理。呵呵
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
你转贴的那篇文章里,我注意到到以下一段内容,你的结论是否不太全面。。。
那么到底DCL是否真的就没有问题了呢?
否,现在的问题还是可见性问题,但问题转到了同一对象不同字段上面,这个问题已经在以前说过了:
public MyObject{
private static MyObect obj;
private Date d = new Data();
public Data getD(){return this.d;}
public static MyObect getInstance(){
if(obj == null){
synchronized(MyObect .class){
if(obj == null)
obj = new MyObject();//这里
}
}
return obj;
}
}
一个线程A运行到"这里"时,对于A的工作区中,肯定已经产生一个MyObect对象,而且这时这个对象已经
完成了Data d.现在线程A调用时间到,执行权被切换到另一个线程B来执行,会有什么问题呢?
如果obj不为null,线程B获得了一个obj,但可能obj.getD()却还没有初始化.
这段我也看到了,他说的和大家关注的应该不是一个问题,大家关注的是构造函数未完成前,也就是对象未被完整生成前是否会被另外的线程看到。文章中很明确的提示是在现在的JMM中,应该不存在这种情况,因为未同步的代码是不会被别的线程看到的,只有执行过构造函数的对象才会被复制到主存区。而这一段的问题是存在另一个私有对象d,因为它并不是在构造函数里初始化的,所以不能保证在MyObject的构造函数完成后(对象生成时),d被初始化了。这时在工作区里会有两个对象,d和MyObject,MyObject持有一个d的引用,当MyObject被复制到主存区的时候,d如果还没有被复制过去,就会发生d未被初始化的问题,所以我觉得这个问题应该是说,必须保证构造函数被执行后,这个对象就是可用的,事实上这也是构造函数的意义所在吧。所以我觉得这与单例的懒汉模式是否安全是没有什么关系的,而是和一个对象是否会有不安全对象被逸出有关。不知我这样理解是不是有问题~~
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
如果真像这篇文章: http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所说那样的话,1.2或以后的版本就不会有问题了,但这个规则是JMM的规范吗?谁能够确认一下。
确实, 在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,
初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.
但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
向主存储区复制赋值时,其它线程绝对不可能见到这个过程.而这个字段复制到主存区的过程,更不会有分配空间后
没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作
存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource
为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见.
另外,文章中的第一个双重检测是可以的,这我 也成认是没有问题,因为JDNI实例早已存在,这里的双重正像文中所说那样只是为了减少查找的次数,这与我们讨论的JMM没有关系,这只能说在不同的应用场景中是不一样的。
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
在2001年,双检锁就已经安全了的结论不是那么容易下的吧。
请参考这篇发布于2004年的文章,里面提及了双检查锁存在的问题及JMM的修复。
http://www.ibm.com/developerworks/cn/java/j-jtp02244/
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
你转贴的那篇文章里,我注意到到以下一段内容,你的结论是否不太全面。。。
那么到底DCL是否真的就没有问题了呢?
否,现在的问题还是可见性问题,但问题转到了同一对象不同字段上面,这个问题已经在以前说过了:
public MyObject{
private static MyObect obj;
private Date d = new Data();
public Data getD(){return this.d;}
public static MyObect getInstance(){
if(obj == null){
synchronized(MyObect .class){
if(obj == null)
obj = new MyObject();//这里
}
}
return obj;
}
}
一个线程A运行到"这里"时,对于A的工作区中,肯定已经产生一个MyObect对象,而且这时这个对象已经
完成了Data d.现在线程A调用时间到,执行权被切换到另一个线程B来执行,会有什么问题呢?
如果obj不为null,线程B获得了一个obj,但可能obj.getD()却还没有初始化.
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
谢谢,这个解释非常清楚.
看看这篇吧,你们的理解已经是老黄历了~~
事实上,双检锁已经安全了~~
第三步可以看作一个赋值语句,只不过是调用构造函数初始化在付值语句之后。另外一个线程得到锁后就看到当前的instence已经不是null了就直接返回了,这个时候有可能第一个线程初始化工作做了一半,或者没有做。这样后面的线程得到的对像就会有问题。我感觉是这样的。
那是不是JAVA多线程里都不敢写new了???
http://www.ibm.com/developerworks/cn/java/j-dcl.html
在effective java里的推荐解决方法是用enum写单例,延迟加载,序列化问题都能解决
是的,这与加不加上retrun是没有太大的关系的
最主要的一点就是,线程可能从任何点切换,这个点可能是某两个语句中,也有可能是从语句中中断,这也是完全有可能的。
如果这样,那第二种的时候,我也在new的时候切换,那不是也不能保证线程安全了吗?
ps:我很少做多线程的东西,不是很了解线程安全
第二种不会的,因为共享的代码块都是放在同步块里的,即在该同步块执行完成后,才能让其他也基于该同步块锁的线程获取锁。双重检测不成立最主要的原因就是 检测 条件不有同步,所以其他线程在执行第一层检测条件时就可能从正在创建对象的线程中抢占CPU,所以就造成了失效
最主要的一点就是,线程可能从任何点切换,这个点可能是某两个语句中,也有可能是从语句中中断,这也是完全有可能的。
如果这样,那第二种的时候,我也在new的时候切换,那不是也不能保证线程安全了吗?
ps:我很少做多线程的东西,不是很了解线程安全
是不是就能解决问题?
肯定是不行的,因为本质你还是有这一句 “instance = new Singleton(); ”这样的赋值语句,所以还是有可能从这条语句中中断切换到另外一线程,而不是说从“return instance;”语句中切换到另一线程,不过我也不知道 return instance; 这样的语句是不是原子性的,但至少你这种必法是起不了作用的。
最主要的一点就是,线程可能从任何点切换,这个点可能是某两个语句中,也有可能是从语句中中断,这也是完全有可能的。
最大的原因就是 Object o = new Object(); 这样的赋值在 JMM (Java 内存模型)里不一条指令来完成,所以表面上看上去是一条,但实质是由 一串指令 来完成,所以就有可能从 这些指令中切换到另一个线程。所以就导致双重检测失效
这样的写法是能避免无序写入的问题。因为别的线程进入不了方法体,除非当前线程释放锁。这样就能确保实例化完成。我的理解对吗?
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
听说在1.5版本后修改了JMM,应该是可以的,因为加上volatile后, instance = new Singleton(); 则VM一定是有序执行指定,即先实例化后赋值,所以不会出现以前先赋值后实例化的问题
另外,如果程序中使用了 synchronized 键字作用于了共享资源,则不再需要volatile来修饰共享变量,因为synchronized 具有了原子性,可见性、有序性。
正如你所说的用ThreadLocal能解决这个问题,担是我认为你的代码里面的写法是不是有问题。
public class Singleton {
private static final ThreadLocal perThreadInstance = new ThreadLocal();
private static Singleton singleton ;
private Singleton() {}
public static Singleton getInstance() {
if (perThreadInstance.get() == null){
// 每个线程第一次都会调用
createInstance();
}
return singleton;
}
private static final void createInstance() {
synchronized (Singleton.class) {
if (singleton == null){
singleton = new Singleton();
}
}
perThreadInstance.set(perThreadInstance);
}
}
倒数第三行perThreadInstance.set(perThreadInstance);应该写成perThreadInstance.set(singleton);
除此之外每一个线程都会有一个singleton的副本这样一样会造成资源浪费。本来单态模式就是想节省资源的,这样与模式的初衷不相符吧。
还有这种ThreadLocal方式的解决方案能不能应用在分布式情形,不同的JVM,ClassLoader?
写成你那样我想也没有问题的,因为这里的ThreadLocal最主要的作用就是一个标识,它只是用来标示当前线程是否已经请求或使用过singleton实例,至于perThreadInstance.set(XX)成什么值我想并不重要,关键要只要访问过或使用过就将其值设成非null。
你所说的资源问题在这里不会有问题,不管是我的那种写法还是你建义的那种,都不会有资源问题,因为这里的键是共享的,不同的线程都会使用同一个perThreadInstance或你的singleton放入每个线程的ThreadLocalMap里面。
至于分布式问题,我真还没有考虑过,我想如果这种实现不能,那么其他方式也是有问题。
最后不同的ClassLoader肯定会有不同的singleton实例,这个可以从上面的测试可以看出来。
坦白的说这样真不行,因为编译通过不了,你把返回语句放在if里面,后面你还得处理返回问题。其实说白了我刚才所说的情况也只是有可能发生的。如果在释放锁之前完成对像的初始化就不会有什么问题。担是我说的那种情况也是有可能发生的,线程的问题并不是每一次测试都能发现的,至于什么时候开始初始化的工作我想只有JVM才知道吧。
简单概括你的问题,如果初始化发生在释放锁之前不会有什么问题,如果初始化发生在释放锁之后就有可能有问题。
编译是不会有问题的,因为我现在的项目就用到了第三种方式,我按上面那样该,没有问题
我认为这样一样还有可能会有问题,此时返回的instance也是有可能没有初始化的。
是不是就能解决问题?
坦白的说这样真不行,因为编译通过不了,你把返回语句放在if里面,后面你还得处理返回问题。其实说白了我刚才所说的情况也只是有可能发生的。如果在释放锁之前完成对像的初始化就不会有什么问题。担是我说的那种情况也是有可能发生的,线程的问题并不是每一次测试都能发现的,至于什么时候开始初始化的工作我想只有JVM才知道吧