1. 适用场景
单例模式可能是众多设计模式中相对最简单的一种了。它能够保证在一个jvm虚拟机中单例类只有一个实例。通常,在应用软件系统中,线程池,缓存,日志,工具或其他拥有管理功能的类都被实现为单例,因为他们在整个系统中只需要存在一份,也只需要对外提供一个全局的访问点即可。
单例的实现有三种方式:
- 懒汉式
- 饿汉式
- 登记式
登记式的底层实现依然是懒汉式或饿汉式(将不同类的单例们存放在一个内部的Map中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,先登记,再返回),因此这里就不做讨论了。我们主要来看前两种,也是大家见得最多的两种。
2. 代码实现
2.1 懒汉式
public class Singleton {
private Singleton() {}
//全局单例
private static Singleton instance =null;
//静态方法
public static Singleton getInstance() {
if (instance == null) { //(1)
instance = new Singleton();
}
return instance;
}
}
上面的代码就是一个最简单的懒汉式单例实现。通过private访问修饰符限定构造器对外不可见,因此无法在外部手动创建一个Singleton实例,同时对外提供一个全局的访问点getInstance方法,以统一的控制对象的获取。这个获取过程在内部保证了对象只会创建一份。注意,这里的全局单例指的是在同一个jvm中的全局单例。
要指出的是,实际上即使构造函数被设置为private,java中的反射机制(如通过setAccessible方法设置方法可见并随后直接调用构造器)仍然可以对其进行调用并实例化一个单例类,这里对这种情况暂不做考虑。
上述的懒汉式单例实现在多线程并发的情况下是不安全的,因为多个线程可能同时执行到(1)处,发现不存在单例实例,于是各自构造器出一个实例,单例存在多个副本,状态不一致了,也就失去了其作用。
那么如果想要把上面的代码变成线程安全的,如何做呢?
有三种方法:
getInstance方法上加同步
public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
double check:双重判断
//全局单例 //使用volatile关键字来声明单例对象(new Singleton()是非原子操作) private static volatile Singleton instance = new Singleton(); public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { //双重检查防止再次new出实例 instance = new Singleton(); } } } return instance; }
使用静态内部类改造
private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } public static final Singleton getInstance() { return LazyHolder.INSTANCE; }
方法3既可以实现线程安全(类加载时已自行实例化INSTANCE),又能够避免同步带来的性能影响。
2.2 饿汉式
private Singleton() {}
//全局单例
private static Singleton instance = new Singleton();
//静态方法
public static Singleton getInstance() {
return instance;
}
饿汉式与上面的方法3有点相似,在类初始化时就已经创建好一个静态的全局单例供系统使用,并且jvm的类加载机制保证了这个类只被加载一次,因此它也是线程安全的。
3. 思考与总结
饿汉式依靠jvm的类加载机制天生实现了线程安全,而懒汉式本身则是线程不安全的,使用上述的3种方法可以对其改造,其中第3种在性能上更优(因为避免了同步)。
通常,饿汉式的实现方式已经能够满足绝大多数需求,性能不错,代码也少,方便且易理解。