一、单例模式
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,而且还能防止反序列化导致重新创建新的对象。