单例模式
1 概述
1)简介
单例对象的类只允许一个实例存在,并提供一个全局访问点。
2)使用场景
有些对象只需要一个的时候,比如线程池、缓存、日志对象等。如果制造出多个实例,就会导致许多问题的产生。
3)为什么不使用全局变量 ?
虽然Java的静态对象可以做到,但如果将对象赋值给一个全局变量,那么我们必须在程序一开始就创建好对象。而恰巧这个对象非常消耗资源,程序在这次运行过程中又一直没有用到它,就会形成浪费。如果我们利用单例模式,我们就可以在需要时才创建对象(延迟实例化)。
2 实现
2.1 懒汉式
懒汉式单例模式可以保证延迟初始化,但并不保证线程安全,类图如下所示。
public class Singleton {
private static Singleton uniqueInstance; //利用静态对象记录类的唯一实例
private Singleton() {} //私有构造器
//利用getInstance()方法实例化对象,并返回实例
public static Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance= new Singleton();
}
return uniqueInstance;
//其他方法……
}
- 私有的构造器可以确保无法从外部创建实例,只能够通过getInstance()方法创建实例。如果我们不需要这个实例,它就永远不会产生,这就是上文中所说的“延迟实例化”。
- 这只是最基本的实现,它不支持多线程,因为没有加锁 synchronized。当有多个线程同时调用getInstance()方法时,就有可能创建出多个实例。
2.2 错误的双重检查加锁
曾经这是一种被认为“聪明”的技巧,但其实是一种完全错误的优化。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static getInstance() {
//只有第一次实例化时,才会彻底执行以下的代码
if(uniqueInstance == null) {
synchronized (Singleton.class) {
//加入区块后,再检查一次
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
为什么错误呢?
因为在类的初始化时,其实发生了如下的步骤:
- 分配对象的内存空间
- 初始化对象
- 设置对象实例指向刚分配的内存地址
问题就在于2、3两个步骤在编译的时候可能会被重排序,即代码执行顺序变为了(1,3,2)。导致在多线程情况下A线程正在执行步骤2初始化对象时,而B线程却可能看到一个未被初始化的对象(内存可见性),所以会判断uniqueInstance 不为null,进而再去创建一次对象,导致Singleton被创建了两次。
2.3 volatile双重检查加锁
方式一时加上volatile关键字,以禁止创建对象的2、3步骤被重排序。这种方案需要JDK5及以上版本。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static getInstance() {
//只有第一次实例化时,才会彻底执行以下的代码
if(uniqueInstance == null) {
synchronized (Singleton.class) {
//加入区块后,再检查一次
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
2.4 内部类方式
使用内部类这种方式同样能够保证在延迟初始化与多线程安全。线程安全的原因是,每一个类或接口,都有一个唯一的初始化锁,当类初始化时会进行加锁,以保证只有一个线程进入,初始化完成后释放锁。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
// 导致SingletonHolder类被初始化
return SingletonHolder.instance;
}
}
3 总结
文中共提到了三种实现单例模式的方式(一种线程不安全的懒汉式实现,一种线程安全的volatile双锁式实现,一种线程安全的内部类实现)。关于单例模式还有更多种的实现方式,当考虑使用何种方式时,这需要先确定在性能和资源上的限制,然后再选择适合自己的程序的方式去实现。