单例模式之懒汉单例(延迟初始化)多线程再解析
1、多线程下的懒汉单例:
public class Lazysingleton {
private static Lazysingleton m_instance = null;
// 私有默认构造方法,外界无法直接实例化
private Lazysingleton() {
}
// 静态工厂方法
public static Lazysingleton getInstance() throws InterruptedException {
// 延迟加载
if (m_instance == null) {
// 模拟创建对象的准备工作
Thread.sleep(3000);
m_instance = new Lazysingleton();// 初始化这个单例
}
return m_instance;
}
}
public class MyThread extends Thread {
@Override
public void run() {
try {
System.out.println(Lazysingleton.getInstance().hashCode());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class TestLazy1 {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
打印结果说明创建出来三个对象,并不是单例,多线程下懒汉单例是非线程安全的。
2、多线程下单例模式非线程安全的解决方案:
2.1、声明synchronized关键字,实现同步方法
加入同步方法得到相同实例对象,此方法运行效率非常低,是同步运行,下一个线程想要取得对象,必须等上一个线程释放锁后,才能运行。
2.2、使用同步代码块
与使用synchronized同步方法一样是同步运行,效率非常低。得到相同实例对象。
2.3、部分代码上锁,进行单独同步,非线程安全
2.4、使用DCL双重检查锁定
使用DCL双重检查锁定成功解决懒汉模式的多线程问题,DCL也是大多数多线程结合单例模式使用的解决方案。
DCL是常见的延迟初始化技术,但有一个错误的用法。使用DCL需要一些技巧。
存在错误的根源:
-
a.多线程试图在同一时间创建对象,会通过加锁来保证只有一个线程创建对象。
-
b.在对象创建好后,执行getInstance()方法将不需要获取锁,直接返回创建好的对象。
问题:当代码读取到m_instance不为空,m_instance引用的对象有可能还没有完成初始化。就会出出现问题。
m_instance = new Lazysingleton();
可以分解为:
-
memory=allocate();1.分配对象的内存空间
-
ctorInstance(memory);2.初始化对象
-
instance=memory;3.设置instance指向刚分配的内存地址
在Java内存模型中为了优化代码会重排代码,会导致线程看到一个还没被初始化的对象。
3、线程安全的延迟初始化方案:
3.1、基于volatile的解决
声明volatile,初始化代码重排就会被禁止,此方案是通过禁止代码重排来实现线程安全的延迟加载。
创建对象的过程,实例化对象一般分为三个过程。
-
1、分配内存空间。
-
2 、初始化对象。
-
3 、将内存空间地址赋值给对象的引用。
但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下
-
1、分配内存空间
-
2、将内存空间的地址赋值给对应的引用
-
3、初始化对象
如果不加volatile的话,可能线程1在初始化的时候重排序了,线程2看到singleton != null,已经返回singleton,其实线程1还没有完成初始化,仅仅只不过是分配了内存空间而已!