首先先实现两个最简单的单例模式:
1.饿汉式:
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton;
public static Singleton getInstance() {
return instance;
}
}
这种方式基于classloder机制避免了多线程的同步问题。instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载。比如:单例中定义了一个静态变量,在使用该静态变量的时候,会导致instance类被实例化,但是类的初始化只有一次,还是能保证instance实例只会被创建一次。
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.懒汉式(线程不安全):
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance==null) {
return new Singleton();
}
return instance;
}
}
这种方式很明显在多线程的时候不能够去使用,可能在不同线程中会同时初始化一个对象。
3.懒汉式(线程安全):
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance==null) {
synchronized (Singleton.class) { //1
if (instance==null) { //2
instance = new Singleton();//3
}
}
}
return instance;
}
}
这是双重检查锁定的方式保证线程安全。
双重检查锁定背后的理论是:
在 //2 处的第二次检查使(如清单 3 中那样)创建两个不同的 Singleton 对象成为不可能。假设有下列事件序列:
- 线程 1 进入getInstance()方法。
- 由于 instance为null,线程 1 在 //1 处进入 syncronized块。
- 线程 1 被线程 2 预占。
- 线程 2 进入getInstance()方法。
- 由于instance仍旧为null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。
- 线程 2 被线程 1 预占。
- 线程 1 执行,由于在 //2 处实例仍旧为null,线程 1 还创建一个Singleton对象并将其引用赋值给 instance。
- 线程 1 退出syncronized 块并从 getInstance()方法返回实例。
- 线程 1 被线程 2 预占。
- 线程 2 获取 //1 处的锁并检查instance是否为null.
- 由于instance是非null的,并没有创建第二个Singleton对象,由线程 1 创建的对象被返回。
由以上可知,理论上双重检查锁定保证了线程安全性,而且有一定的优化,只在创建Singleton的代码块部分加锁。
看两个多线程操作单例的实例:
(1)没有双重检测锁定的情况下:
public class DubbleSingleton {
private static DubbleSingleton ds;
public static DubbleSingleton getDs(){
if(ds == null){
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (DubbleSingleton.class) {
ds = new DubbleSingleton();
}
}
return ds;
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 结果:
1745684894
1980094282
1091862201
*/
(2)有双重检测锁定的情况下:
public class DubbleSingleton {
private static DubbleSingleton ds;
public static DubbleSingleton getDs(){
if(ds == null){
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (DubbleSingleton.class) {
if(ds == null){
ds = new DubbleSingleton();
}
}
}
return ds;
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 结果:
284406170
284406170
284406170
*/
可以很明显的看到,第二种方式保证了线程安全,多个线程操作了同一个对象实例。
但是双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因,关于这一部分的内容,大家可以参考《深入Java虚拟机》这本书,有很详细的解释。
我们的优化方式是把private static Singleton instance = null;
修改为 private volatile static Singleton instance = null;
这里的 volatile
关键字主要是为了防止指令重排。
如果不用 ,singleton = new Singleton();
,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将
singleton
对象指向分配的内存地址。(3)
加上 volatile
是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
4.静态内部类
public class Singletion {
private Singleton() {}
private static class InnerSingleton {
private static Singletion single = new Singletion();
}
public static Singletion getInstance(){
return InnerSingletion.single;
}
}
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不停的是:饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为InnerSingleton类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载InnerSingleton类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理。