在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。
更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。
定义
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。
单例模式的要点有三个:
- 单例类只能有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
结构
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
以懒汉式为例,UML类图如下:

时序图如下:

实现
Ⅰ饿汉式-静态常量
public class Singleton {
// 构造器私有化,用户无法通过new方法创建该对象实例
private Singleton() {
}
// 在静态初始化器中创建单例实例,这段代码保证了线程安全
private static Singleton uniqueInstance = new Singleton();
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return uniqueInstance;
}
}
优点:这种写法比较简单,在类装载的时候就完成实例化,避免了线程同步等问题,是线程安全的。
缺点:在类装载的时候就完成实例化,没有达到懒加载(Lazy Loading)的效果。如果从始至终从未使用过这个实例,则会造成内存浪费。
这种单例模式可用,但可能造成内存浪费。
Ⅱ 饿汉式-静态代码块
public class Singleton {
private Singleton() {}
private static Singleton uniqueInstance;
// 静态代码块中创建实例
static {
uniqueInstance = new Singleton();
}
public static Singleton getInstance() {
return uniqueInstance;
}
}
与上面类似,也是在类装载的时候就完成实例化,只不过将类实例化的过程放在了静态代码块中。
这种单例模式可用,但可能造成内存浪费。
Ⅲ 懒汉式-线程不安全
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
// 提供一个静态的公有方法,当使用到该方法时,才去创建uniqueInstance
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
优点:起到了**懒加载(Lazy Loading)**的效果,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。
缺点:线程不安全。如果多个线程能够同时进入 if (uniqueInstance == null)
,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton();
语句,这将导致实例化多次 uniqueInstance
实际开发中,不要使用。
Ⅳ 懒汉式-线程安全
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
// 提供一个静态的公有方法,当使用到该方法时,才去创建uniqueInstance
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
优点:线程安全。只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。
缺点:效率低。每个线程在想获得类的实例时候,执行 getInstance()方法都要先进行同步,即使 uniqueInstance 已经被实例化了,这会让线程阻塞时间过长。
实际开发中,不推荐使用。
Ⅴ 懒汉式-线程安全-双重校验锁
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {// 避免已经实例化后的加锁操作
synchronized (Singleton.class) {
if (uniqueInstance == null) {// 避免多个线程同时进行实例化操作
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
**双重校验锁(Double-Check Locking)**概念是多线程开发中常使用到的。假如在uniqueInstance == null
的情况下,两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。可以肯定都是,两个线程都会执行 uniqueInstance = new Singleton();
这条语句,只是先后的问题,也就是说肯定会有两次实例化。
因此必须需要使用两个 if 语句:第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作;第二个 if 语句进行了加锁,只允许一个线程进入,保证了线程安全,避免出现uniqueInstance == null
时两个线程同时进行实例化操作问题。
uniqueInstance
采用 volatile关键字修饰也是很有必要的,因为 JVM了性能优化,可能会指令重排。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。而使用 volatile 修饰uniqueInstance后会引入内存屏障(Memory Barrier),保证了JVM的可见性与有序性。(双重校验锁详解可参考之前写的【并发编程】volatile一文)
总之,双重检验锁保证了线程安全,同时懒汉式保证了延迟加载。
效率较高,推荐使用。
Ⅵ 静态内部类
class Singleton {
//构造器私有化
private Singleton() {
}
// 静态内部类,该类中有一个静态属性 Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 提供一个静态的公有方法,直接返回 SingletonInstance.INSTANCE
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()
方法从而触发 SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。
这种方式不仅具有延迟初始化的好处,利用静态内部类特点实现了延迟加载,而且由 JVM 提供了对线程安全的支持。
效率高,推荐使用。
VII 枚举
enum Singleton {
INSTANCE; //属性
public void doSomeTing() {
System.out.println("通过枚举方法实现单例");
}
}
使用:
public class EnumSingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomeThing();// 通过枚举方法实现单例
}
}
这种方式是Effective Java 作者Josh Bloch 提倡的方式。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。详细分析可参考Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式一文。
推荐使用。
JDK
总结
优点
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;Java、运行环境中提供了自动垃圾回收的技术,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这会导致对象状态的丢失。
适用场景
需要频繁的进行创建和销毁、或者创建时耗时过多或耗费资源过多但又经常用到的对象;工具类对象;频繁访问数据库或文件的对象(比如数据源、session工厂等)。