单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
总结:单例模式顾名思义就是单例类只能有一个实例,且该类需自行创建这个实例,并对其他的类提供调用这一实例的方法。
主要优点:
主要缺点:
在以下情况下可以考虑使用单例模式:
常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁、静态内部类和枚举单例。
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
优点和缺点:
优点:
缺点:
public class EagerSingleton {
/**
* 私有实例,静态变量会在类加载的时候初始化,是线程安全的
*/
private static EagerSingleton eagerSingleton = new EagerSingleton();
/**
* 私有构造方法
*/
private EagerSingleton() {
}
/**
* 唯一公开获取实例的方法(静态工厂方法)
*
* @return
*/
public static EagerSingleton getEagerSingleton() {
return eagerSingleton;
}
}
懒汉式就是“比较懒”,就是在用到的时候才去检查有没有实例,如果有则直接返回,没有则新建。
优点和缺点:
优点:
缺点:
public class LazySingleton {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static LazySingleton lazySingleton;
/**
* 私有构造
*/
private LazySingleton() {}
/**
* 唯一公开获取实例的方法(线程不安全)
*
* @return
*/
public static LazySingleton getInstance() {
if(lazySingleton == null) { // 使用的时候加载
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
改良版线程安全的懒汉模式:
public class LazySingleton {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static LazySingleton lazySingleton;
/**
* 私有构造
*/
private LazySingleton() {}
/**
* 唯一公开获取实例的方法(线程安全,调用效率低)
*
* @return
*/
public synchronized static LazySingleton getLazySingleton() {
if(lazySingleton == null) { // 使用的时候加载
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
上面代码中,通过关键字synchronized声明公共的获取实例的方法 getLazySingleton(),可以确保线程安全,能做到延迟加载,但是效率不高。
public class SingletonByDoubleCheckLock {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static SingletonByDoubleCheckLock singleton;
/**
* 私有构造
*/
private SingletonByDoubleCheckLock(){}
/**
* 唯一公开获取实例的方法
*
* @return
*/
public static SingletonByDoubleCheckLock getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(SingletonByDoubleCheckLock.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new SingletonByDoubleCheckLock();
}
}
}
return singleton;
}
}
上面这段代码已经看似完美,但是还存在一个问题:指令重排序。
指令重排序:计算机在执行程序时候,为了提高代码、指令的执行效率,编译器和处理器会对指令进行重新排序,一般分为编译器对于指令的重新排序、指令并行之间的优化、以及内存指令的优化。
关于指令重排序的详细内容,请移步观看我的文章——JMM(Java 内存模型)详解,还没有更完,后续会更新文章链接。
解决方案:使用volatile防止指令重排序
创建一个对象,在JVM中会经过三步:
为singleton分配内存空间
初始化singleton对象
将singleton指向分配好的内存空间
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报空指针异常。文字较为晦涩,可以看流程图:
改良后的代码:
public class SingletonByDoubleCheckLock {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载),使用volatile关键字,禁止指令重排序
*/
private volatile static SingletonByDoubleCheckLock singleton;
/**
* 私有构造
*/
private SingletonByDoubleCheckLock(){}
/**
* 唯一公开获取实例的方法
*
* @return
*/
public static SingletonByDoubleCheckLock getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(SingletonByDoubleCheckLock.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new SingletonByDoubleCheckLock();
}
}
}
return singleton;
}
}
该模式利用了静态内部类延迟初始化的特性,来达到与双重校验锁方式一样的功能。
public class SingletonByStaticInnerClass {
/**
* 私有静态内部类
*/
private static class InnerClass{
// 初始化实例
private final static SingletonByStaticInnerClass INSTANCE = new SingletonByStaticInnerClass();
}
/**
* 私有构造
*/
private SingletonByStaticInnerClass() {}
/**
* 唯一公开获取实例的方法(静态工厂方法)
*
* @return
*/
public static SingletonByStaticInnerClass getInstance() {
return InnerClass.INSTANCE;
}
}
优点:
我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,
之后再也不会实例化
1. 枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
2. 在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,
在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题;
为了防止多线程环境下,因为指令重排序导致变量报空指针异常,需要在单例对象上添加volatile关键字防止指令重排序;
最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。