单例模式的定义与特点
单例(Singleton)模式的定义:指一个类只有一个实例,且这类能自行创建这个实例的一种模式。例如,Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致错误。
单例模式有3个特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点。
单例模式的结构与实现
单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new构造函数”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
1. 单例模式的结构
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
其结构如图 1 所示。
一、单例模式
1、定义
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类
2、特点
单例类只有一个实例
单例类必须自己创建自己的唯一实例
单例类必须给索引其他对象提供这个实例
二、创建单例模式的方式
1、懒汉式,线程不安全
懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不会着急。会一直等到马上要使用对象才会实例化。
public class Singleton{
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance(){
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码简单明了,而且使用了懒加载模式,但是存在致命的问题。当多个线程并行调用getInstance()的时候,就会创建多个实例,即在多线程下不能正常工作。
2、懒汉式,线程安全
为了解决上面的问题,最简单的方法是将整个getInstance()方法设为同步(synchronized)
public static synchronized Singleton getInstance(){
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用getInstance()方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
3、双重检验锁
双重检验锁模式,是一种使用同步块加锁的方法。程序员称其为双重检验锁,因为会有两次检查instance== null
,一次是在同步块外,一次是在同步块内。
为什么在同步块内还要再检验一次?因为可能多个线程一起进入同步块外的if,如果在同步块内不进行二次检验的话就会生成对个实例。
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
但是这段代码看起来很完美,很可惜,它存在问题。主要在于instance = new Singleton()这句,它并非一个原子操作,实际上在JVM中这句话大概做了三个步骤
- 给instance分配内存
- 调用Singleton的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步instance就非null)
但是在JVM的即时编译器存在指令重排序的优化。因而上述第二步和第三步的顺序是不能得到保证的,最终的执行顺序可能是1-2-3也可能是1-3-2,。如果是后者,则在3执行完毕,2未执行前,被线程二抢占,这时instance已经非null(但还没有初始化),所以线程二会直接返回instance,然后使用,此时会报错。
因而需要将instance变量声明为volatile,就可禁止指令重排序。
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
4、饿汉式,static final field
饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,于是在装载类的时候就创建对象实例。
这种方法非常简单,因为单例的实例被声明成static和final变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
缺点是它不是一种懒加载模式,单例会在加载类后一开始就被初始化,及时客户端没有调用getInstance()方法。
饿汉式的创建方式在一些场景中无法使用,如Singleton实例的创建时依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那么这种单例写法就无法使用了。
5、静态内部类 static nested class
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类SingletonHolder,在该内部类中定义了一个static类,的变量INSTANCE,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只会初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。
6、枚举
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是生命枚举实例的通常做法。
public enum EasySingleton{
INSTANCE;
}
我们可以通过EasySingleton.INSTANCE
来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。