单例模式(
Singleton Pattern
)是一种创建型设计模式。
它确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
1. 饿汉式
所谓饿汉式,就是直接创建出类的实例化,然后用private
私有化,对外只用静态方法暴露。
1.1. 静态变量
步骤
- 构造器私有化
- 类的内部创建对象
- 对外暴露一个静态的共有方法获取该对象
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.内部创建对象实例
private static final Singleton INSTANCE = new Singleton();
//3.对外公有的静态方法获取该对象
public static Singleton getInstance() {
return INSTANCE;
}
}
1.2. 静态代码块
将类的实例化放到静态代码块中的写法,基本同上。
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.内部创建对象实例
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象
public static Singleton getInstance() {
return INSTANCE;
}
}
2. 懒汉式
所谓懒汉式,就是在需要调用的时候再创建类的实例化。
2.1 线程不安全
起到了懒加载效果,但是只能在单线程使用,多线程会不安全,因为当多个线程并发同时判断instance
为空时,就会相应的实例化多个对象。
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.内部创建对象实例
private static Singleton instance;
//3.对外公有的静态方法获取该对象
public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
2.2 线程安全
上述线程不安全,通过加同步锁(synchronized
)的机制进行优化,保证每次只有一个线程可以操作当前对象,确保了线程安全。
这样虽然解决了线程安全,但实例化操作只做一次,而获取实例(即getInstance
)的操作是很多次的,把调用的方法加上同步,会大大降低效率。
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.内部创建对象实例
private static Singleton instance;
//3.对外公有的静态方法获取该对象
public static synchronized Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
2.3. 双重检查锁
上述代码效率低,因此在同步前判断一下有没有实例化就可以了,若没有实例化则用同步方法new
一个,否则直接return
即可。即所谓的双重检查。
需要用到关键字volatile
,防止指令重排。如果不用volatile
关键字,就会和线程不安全情形一样,在if
判断那会有并发。
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.内部创建对象实例
private static volatile Singleton instance;
//3.对外公有的静态方法获取该对象
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
(1)双重检查锁模式下,为什么要进行两次判断?
假设有两个线程A
、B
。两个线程都去请求单例模式下类的实例,当第一个判断的时候,两个线程都会进入判断代码块中进行锁的抢占,最终A
抢占到了锁,那么B
只能在加锁的代码块外部进行等候,这个时候A
创建了对象的实例,完成功能后归还了锁,这个时候线程B
马上抢占到了锁,然后进入内部代码块,假设在这里没有第二次判断的话,线程B就会再次创建一个新的对象,所以,要在这里再加一次判断。
(2)双重检查锁模式下,为什么要使用volatile
关键字?
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但是,JVM
在实例化对象的时候会进行优化和指令重排序操作,在多线程的情况下,就可能会出现空指针问题。
对于上述对象的创建主要分为三个部分:
①:分配对象的内存空间。
②:初始化对象。
③:设置instance
指针指向对象所在的内存空间。
为了提高性能JVM
在实例化对象的时候会进行优化和指令重排序操作,就是说有可能将上述的第 8 行和第 11 行代码进行顺序的交换。
就是说当上面A
线程得到锁以后,这时候还没有初始化对象,就先设置了instance
指针指向了对象所在的内存空间,A
线程在设置了instance
指针指向了对象所在的内存空间以后就归还了锁,线程B
这个时候拿到锁以后,检查到对象不为空,直接返回了线程A
创建的对象,但是这个时候线程A
还没有完成对象的初始化,所以就导致了线程B
拿到的对象是一个空对象,就会出现空指针的问题。
因此解决上述的问题,只需要使用volatile
关键字, volatile
关键字可以保证可见性和有序性,这个关键字禁止了对当前修饰的变量上下文重排序,保证了方法的可靠性。
2.4. 静态内部类
Singleton
类:
public class Singleton {
//1.私有化构造函数
private Singleton() {
}
//2.静态内部类,包含一个静态属性:Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
第一次加载Singleton
类时不会去初始化INSTANCE
,只有第一次调用getInstance
,虚拟机加载SingletonHolder
并初始化INSTANCE
,这样不仅能确保线程安全,也能保证Singleton
类的唯一性。
因此静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
3. 单例模式存在的问题
3.1. 单例模式被破坏
在上述定义的单例类(Singleton)中正常的使用的情况下只可以同时只有一个对象存在,但是存在着一些操作可以破坏这种现象,使得上述单例模式可以创建多个对象(枚举方式除外)。
这两种方式,分别是序列化和反射:
(1)序列化反序列化破坏单例模式
在这里使用性能较好的内部类方式的单例模式,来检验一下,序列化与反序列化是否会创建不同的对象。
Singleton
类:
public class Singleton implements Serializable {
//1.私有化构造函数
private Singleton() {
}
//2.静态内部类,包含一个静态属性:Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Test
类:
public class Main {
public static void deserialization() throws IOException, ClassNotFoundException {
//1.反序列化
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
//2.将类转化
objectOutputStream.writeObject(Singleton.getInstance());
System.out.println(Singleton.getInstance());
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
//3.读出类,变为一个新的类
Singleton singleton = (Singleton) objectInputStream.readObject();
System.out.println(singleton);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
deserialization();
}
}
运行结果:
根据运行结果,表明序列化和反序列化已经破坏了单例设计模式只有一个对象存在的原则。
(2)反射破坏单例模式
还是使用性能较好的内部类方式的单例模式,来检验一下,序列化与反序列化是否会创建不同的对象。
Singleton
类:
public class Singleton implements Serializable {
//1.私有化构造函数
private Singleton() {
}
//2.静态内部类,包含一个静态属性:Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Test
类:
public class Main {
public static void reflex() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
System.out.println(Singleton.getInstance());
//1.反射破坏
Class<Singleton> hungrySingletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
//2.设置私有可调用
declaredConstructor.setAccessible(true);
System.out.println(declaredConstructor.newInstance());
}
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
reflex();
}
}
运行结果:
根据运行结果,表明反射也破坏了单例设计模式只有一个对象存在的原则。
3.2. 原因分析
对于序列化与反序列化破坏单例模式的问题,主要是通过readObject()
方法,出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。
对于反射破坏单例模式是因为单例模式通过setAccessible(true)
指示反射的对象在使用时,取消了Java
语言访问检查,使得私有的构造函数能够被访问。而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。而反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。
3.3. 问题解决
(1)序列化、反序列方式破坏单例模式的解决方法
在readObject()
方法的调用栈的底层方法中有这么两个方法:
hasReadResolveMethod
:表示如果实现了serializable
或者externalizable
接口的类中包含readResolve
则返回true
。
invokeReadResolve
:通过反射的方式调用要被反序列化的类的readResolve()
方法。
因此,原理也就清楚了,主要在Singleton
中定义readResolve()
方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
Singleton
类:
public class Singleton implements Serializable {
//1.私有化构造函数
private Singleton() {
}
//2.静态内部类,包含一个静态属性:Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//4.防止序列化、反序列化方式破坏单例
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
(2)反射方式破解单例的解决方法
反射是一种暴力获取对象实例的方法,因为他可以直接访问private
修饰的构造函数,所以在对于反射方式破坏单例模式的问题上只能采取被动的防御,既然你能访问我的构造函数,我就在我的构造函数中建立防御机制,不让你通过我的构造函数创建多个实例对象。
Singleton
类:
public class Singleton implements Serializable {
private static volatile boolean flag = false;
//1.私有化构造函数
private Singleton() {
synchronized (Singleton.class) {
if (flag) {
throw new RuntimeException();
}
flag = true;
}
}
//2.静态内部类,包含一个静态属性:Singleton
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//3.对外公有的静态方法获取该对象,直接返回 SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
4. 枚举
其实,使用枚举也能实现单例模式,不仅能避免多线程同步问题,也能防止反序列化重新创建新的对象。
Singleton
类:
public enum Singleton {
INSTANCE
}
Test
类:
public class Main {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
(1)关于枚举类单例模式为什么不会被破坏
其实是因为java
的底层做了很多的方法来防止枚举类单例模式被破坏,其中一点就是无法创建枚举类的实例。