一、初识单例模式
定义
单例模式确保了某个类中只存在唯一的实例,并且自行实例化向整个系统提供这个实例。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。常用于计算机系统中线程池、缓存、日志对象、对话框、打印机、显卡驱动程序对象的设计。
特点
- 单例类只有一个实例
- 单例模式必须自己创建自己的唯一对象,必须构造器私有防止外部创建对象!
- 单例模式必须提供这一实例给所有其他对象
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。
二、存在的问题
多线程模式下使用单例模式需要格外的小心!
如果唯一实例未创建,有两个线程同时调用创建方法,那么他们同时会监测出没有唯一的实例而创建实例,这样会导致两线程创造多个单例实例!违反了单例模式中实例唯一的原则!
解决方法:为知识类是否已经实例化的变量提供一个互斥锁。
三、单例模式八种写法
1、饿汉式(静态常量)【可以使用】
class Hungry{
// 唯一实例
private static final Hungry HUNGRY = new Hungry();
// 不允许外围创建,构造器私有
private Hungry(){}
public static Hungry getInstance(){
return HUNGRY;
}
}
优点:写法较简单,在类装载时完成了加载,不会出现刚刚提到的线程问题。
缺点:在类装载时就完成了加载,可能调用该类的其他方法或静态属性就会完成加载,没有达到Lazy Loading
的效果,如果从始至终都没有使用该实例,就会出现内存资源的浪费
2、饿汉式(静态代码块)【可以使用】
class Hungry {
private static final Hungry INSTANCE;
static {
INSTANCE = new Hungry();
}
private Hungry() {}
public static Hungry getInstance() {
return INSTANCE;
}
}
实现方法与第一种方法相同,因此优缺点也与方法一一致。
3、懒汉式(线程不安全)【不可用】
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:起到了Lazy Loading
的效果,能够做到懒加载节约内存资源。
缺点:很明显在多线程环境下两个线程同时进入if(singleton == null)
语句就会创建多个实例。
4、懒汉式(线程安全,同步方法)【不推荐】
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:解决了第三种实现中的线程不安全问题,同时也能够解决Lazy Loading
问题。
缺点:效率太低了,每一次调用getInstace
方法都需要同步。本来在第一次调用后直接return
就可以,但还是要进行同步特别影响效率。
5、懒汉式(线程不安全,同步代码块)【不可用】
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance{
if(singleton == null){
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
6、双重检查(DCL懒汉式)【推荐】
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check
概念通过两次if(singleton == null)
检查,八正了线程安全,同时实例化代码只会执行一次,后面再次访问,if
判断后会直接返回单例对象。
优点:线程安全;lazy loading
;效率很高;
单例类实例为什么需要添加
volatile
?
JUC中学过指令重排,CPU可能会将原本顺序执行的指令执行顺序打乱,但打乱后指令执行结果不会受到影响,就是指令重排。对于new Singleton()
这个操作本身就不是一个原子性操作,可以分为以下三步
- 分配内存空间,此时该空间内容为空
- 执行构造方法,初始化对象修改内存空间内容
- 将这个对象指向这个空间,此时
sinleton
指针被修改为非空地址
正常情况下按照1 -> 2 -> 3
的顺序执行与按照1 -> 3 -> 2
顺序执行结果不会被影响,因此CPU可能会将指令重排使得执行次序为1 -> 3 -> 2
。
假设有进程A、B,A进程按照1 -> 3 -> 2
顺序执行到 3 时,此时singleton
指向了一个未添加内容的内存地址,即:singleton
非空,但是singleton
指向地址存储值为空。B进程进入函数后进行if(singleton==null)
判断,判断为false
直接返回singleton
,此时返回的引用指向的地址因为进程A还没有执行到 2 操作所以没有值,可能会引发错误。
因此为了防止指令重排带来的一系列问题,该方法创建单例类实例需要添加volatile
防止指令重排。
7、静态内部类【推荐】
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
对于SingletonInstace
只有在调用getInstace()
时才会被加载完成实例化,相较于饿汉式这样的加载方式能够节省内存空间,确保了lazyLoading。
同时静态属性只有第一次类加载时才会初始化,所以JVM也能保证线程的安全性。
优点:线程安全;lazyLoading
;效率高
8、枚举【特别推荐】
为什么是枚举?
前7种方法实现单例模式中,都忽略了Java中一个重要创建对象的方法,反射!
通过反射能够随意修改单例类构造器的访问权限并创建对象,几乎没有方法能够防止反射创建单例模式的实例对象。因此前面7中方法或多或少都会有弊端。
但是反射无法破坏枚举类,在newInstance
方法中有这么一个判断,使得使用枚举类实现的单例模式不会受到反射的影响!
枚举类的所有成员均只有唯一实例,与静态类、静态成员变量相同,都是使用JVM特性实现单例模式。那么现在尝试实现单例模式:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
public static void main(String []args){
Singleton.INSTANCE.doSomthing()
}
如果仍然想通过反射破坏枚举类,通过javap -p .class文件
反编译 .class 文件可以得知(可能会得到无参构造器,但是使用更高级的反编译器jad.exe反编译会得到正确构造器),枚举类中拥有一个双参构造器

使用反射尝试强行创建实例破坏单例
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
singleton.doSomething();
}
得到结果:抛出异常
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at Main.main(Main.java:15)
可以看到使用Enum实现的单例模式能够防治反射恶意破坏!