定义
单件模式确保一个类只有一个实例,并提供一个全局访问点
所谓单件模式,其实就是我们常说的单例模式。
对于线程池、缓存、日志对象等全局只需要一个对象,适合用单件模式来产生。
几种实现
1. 经典单例模式(懒汉式创建)
/**
* 经典单件模式实现
*/
public class Singleton {
private static Singleton singleton;
//私有构造器,使单件类只能通过getInstance()获取
private Singleton() {
}
//保证singleton的唯一实例
public static Singleton getInstance(){
if(singleton == null)
singleton = new Singleton();
return singleton;
}
}
经典的单例模式,教科书上的Demo代码。但这种写法只是用来讲解,实际运用是有问题的(线程不安全)。
需要注意的是,对象只有在需要时(调用getInstance()
方法)才会创建。所以这种模式又被称为“懒汉式”单例模式。
这种延迟实例化的优势在于:对于非常耗费资源的对象,如果程序一开始就创建了它,然后执行过程中一直没使用,会造成浪费。
2. 多线程单例模式(懒汉式创建)
如果我们细究第一种的单例模式,会发现它是线程不安全的。在多线程环境下,单例对象第一次创建时,可能被多个线程重复创建对象
if(singleton == null)
singleton = new Singleton();
如何解决?最简单的方式是给全局获取单例的方法getInstance()
加锁:
/**
* 单件模式加锁实现,保证了单件模式的线程安全
*/
public class SynSingleton {
private static SynSingleton singleton;
//私有构造器,使单件类只能通过getInstance()获取
private SynSingleton() {
}
//保证singleton的唯一实例,加上synchronized保证该方法被调用时的线程安全
public static synchronized SynSingleton getInstance(){
if(singleton == null)
singleton = new SynSingleton();
return singleton;
}
}
这样,单例模式便线程安全了。
但是,我们再思考一下,这个模式仍然有许多问题。首先,加锁对性能的影响很大。而我们其实只用保证单例模式在第一次执行该方法时线程安全。
也就是说,一旦singleton
被创建出来,该方法是不再需要同步的,而现在,每次我们调用getInstance()
方法时,都是同步的。
3. 多线程单例模式(饿汉式创建)
如何解决上一种单例模式实现的弊端?
很简单,既然第一次创建单例时会造成线程安全,那我最开始就创建出单例对象就好了:
/**
* “急创建”单件模式实现,静态变量初始化保证了单件模式的线程安全
*/
public class StaticInitializeSingleton {
private static StaticInitializeSingleton singleton = new StaticInitializeSingleton();
//私有构造器,使单件类只能通过getInstance()获取
private StaticInitializeSingleton() {
}
//保证singleton的唯一实例,加上synchronized保证该方法被调用时的线程安全
public static synchronized StaticInitializeSingleton getInstance(){
return singleton;
}
}
这种初始化类时便创建单例对象被称为“饿汉式”单例模式,大部分情况使用这种写法就行了。
但是,我们发现先前写法里我们引以为傲的“延迟实例化”没了,对于十分耗资源的对象,在程序启动的一刻就创建,然后一直没有用,无疑是会对性能造成影响的。
那么有没有更完美的单例模式呢?
4. 多线程单例模式(双重检查加锁)
/**
* 双重检查加锁单件模式实现
*/
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton singleton;
//私有构造器,使单件类只能通过getInstance()获取
private DoubleCheckSingleton() {
}
//保证singleton的唯一实例
public static synchronized DoubleCheckSingleton getInstance(){
if(singleton == null){
synchronized (DoubleCheckSingleton.class) {
if(singleton == null)
singleton = new DoubleCheckSingleton();
}
}
return singleton;
}
}
这种双重加锁检查的单例模式,即保证了只有在第一次创建单例时加锁,又保证了延迟实例化。
值得注意的是:
- 加锁的代码里面继续判断了
singleton == null
,这是为了防止实例化完成后,先前已经进入if(singleton == null)
代码块的线程,再次创建单例。 volatile
关键字必须使用,原因在与volatile
除了保证线程之间的可见性外,还可以禁止指令重排序优化。
此处直接引用你真的了解volatile关键字吗?中的解释:
为什么要使用
volatile
修饰instance
?主要在于
instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:1.给
instance
分配内存2.调用
Singleton
的构造函数来初始化成员变量3.将
instance
对象指向分配的内存空间(执行完这步instance
就为非 null 了)。但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
最后,由于valatile
关键字出现在JDK1.4之后,所以,该模式不适用于JDK1.4之后的版本。
那,有没有兼容所有JDK的更完美的写法了?
5. 终极解决方案:静态内部类单例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法是《Effective Jave》里面推荐的写法。
这种写法使用JVM的机制来保证了线程安全。只有第一次执行return SingletonHolder.INSTANCE
代码时,静态内部类进行初始化,才会实例化单例,从而实现了“延迟实例化”。
本文总结自:
《Head First设计模式》 第五章:单件模式