单例模式
前言
单例模式大家都很熟悉,基本上做开发的人都知道有这个模式,用途也很明确。但是深挖一下,就会发现好多东西。个人认为,单例模式是创建唯一对象并为其提供全局访问的方法。
单例模式有四种:懒汉式单例、饿汗式单例、静态内部类单例以及枚举式单例。这四种单例模式是一步步过渡的,最开始是懒汉式和饿汉式,两者一个是类使用时初始化、一个是类加载时直接初始化实例。然后就是静态内部类单例,利用静态内部类的特性使得外部类使用时不会加载内部类,调用到内部类的时候才会加载内部类,将懒汗式和饿汉式的优点结合。但是不论是懒汉式、饿汗式还是静态内部类都会被序列化后反序列化以及反射破坏单例,枚举式单例则是解决这一问题的答案。
文章中出现的代码用例可以到GitHub上查看
各类型单例实现
懒汉式单例
懒加载,对象使用时再去创建,私有构造方法并提供一个返回实例对象的方法,方法中对对象双重非空判断并加锁以及volatile关键字修饰对象禁止指令重排保证其线程安全。
实例
/**
* 懒汉式单例:私有构造方法,双重非空校验,创建对象时加锁,volatile关键字禁止指令重排保证创建对象这一步骤的线程安全
*
* @author linmeng
* @version 1.0
* @date 2022年3月28日 16:42
*/
public class LazySingleton implements Serializable {
private static volatile LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (Objects.isNull(lazySingleton)) {
synchronized (LazySingleton.class) {
if (Objects.isNull(lazySingleton)) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
优缺点
优点:类加载时不实例化对象,使用时才对对象进行实例化,减少内存消耗
缺点:使用了同步锁,对性能有一定影响,反射以及序列化后反序列化都能破坏单例
原理
volatile关键字作用
jvm创建对象分为三个步骤
- 为对象分配空间
- 初始化对象
- 将初始化的对象指向分配的空间
jvm在执行语句时,为了提高性能,有可能不按照代码顺序执行。这时候,代码执行顺序有可能变成1 3 2.这个时候一个线程走到3那一步,但是没有初始化对象,另一个线程判断非空,对象不为空,直接返回未初始化的对象进行使用,这就会造成空指针。
但是用了volatile关键字,禁止指令重排,就不会出现这种情况。
饿汉式单例
在程序加载时创建对象,调用时直接返回
实例
/**
* 饿汉式单例:static关键字使得变量在类加载时就初始化,final关键字使得变量不可被修改
* @author linmeng
* @version 1.0
* @date 2022年3月28日 16:55
*/
public class EagerSingleton implements Serializable {
private static final EagerSingleton eagerSingleton = new EagerSingleton();
private EagerSingleton(){}
/**
*
* 直接返回
* @author linmeng
* @date 2021年11月9日 21:44
* @return com.sword.www.designPattern.singleton.EagerSingleton
*/
public static EagerSingleton getInstance(){
return eagerSingleton;
}
}
优缺点
优点:代码逻辑简单易懂
缺点:占用内存,反射、序列化后反序列化都能破坏单例
静态内部类单例
私有构造方法,定义一个静态内部类,内部类中声明对象,再定义一个获取实例的方法获取内部类中的实例。
实例
/**
* 静态内部类单例:外部类加载时不会加载内部类,只有在使用到内部类的时候虚拟机才会加载内部类
* 保证了线程安全,单例的唯一性,延迟了类的实例化
* https://blog.csdn.net/mnb65482/article/details/80458571
* https://juejin.cn/post/6871497501732519949
*
* @author linmeng
* @version 1.0
* @date 2022年3月28日 17:00
*/
public class StaticNestSingleton implements Serializable {
private StaticNestSingleton() {
}
private static class SingletonHolder {
private static final StaticNestSingleton SINGLETON = new StaticNestSingleton();
}
/**
* 直接返回
*
* @return com.sword.www.designPattern.singleton.EagerSingleton
* @author linmeng
* @date 2021年11月9日 21:44
*/
public static StaticNestSingleton getInstance() {
return SingletonHolder.SINGLETON;
}
}
优点:利用虚拟机的机制保证了线程安全。类加载时不会加载内部类,使用时才会加载,相当于懒汉式和饿汉式的优点结合
缺点:会被序列化后反序列化破坏单例
枚举式单例
直接定义一个枚举类。
实例
/**
* https://www.cnblogs.com/happy4java/p/11206105.html
* https://blog.csdn.net/weixin_48358336/article/details/120499442
*
* @author linmeng
* @date 2022年3月28日 17:09
* @return
*/
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("do something");
}
}
优缺点
优点:代码简单,保证了在多线程下的安全性。利用虚拟机机制防止反射、序列化后反序列化
缺点:哪有什么缺点
单例破坏
使用一些特殊手段可以同时实例化多个单例对象,这样就破坏了单例模式。破坏单例的手段有这几种:
- 多线程:多线程下看获取到的单例对象是否为同一个,这个文中的单例代码均不受其破坏
- 反射:反射使用私有构造除枚举式单例可以不被破坏,其他单例均需要在代码层面处理
- 序列化后反序列化:将对象序列化成文件,再对其反序列化
多线程破坏
使用CountDownLatch使得多个线程同时获取单例对象,通过检查对象地址判断是否创建了多个单例对象从而破坏单例
测试用例
public void concurrentDestroy() throws InterruptedException, ExecutionException {
// 定长线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 使得多个线程同一时刻启动
CountDownLatch countDownLatch = new CountDownLatch(5);
// 1:饿汉式
System.out.println("饿汉式地址打印");
List<Future<EagerSingleton>> eagerSingletonFutureList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Future<EagerSingleton> future = executorService.submit(() -> {
countDownLatch.countDown();
return EagerSingleton.getInstance();
});
eagerSingletonFutureList.add(future);
}
countDownLatch.await();
// 对象地址打印
print(eagerSingletonFutureList);
// 2:懒汉式
System.out.println("懒汉式地址打印");
List<Future<LazySingleton>> lazySingletonFutureList = new ArrayList<>();
CountDownLatch countDownLatch1 = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Future<LazySingleton> future = executorService.submit(() -> {
countDownLatch1.countDown();
return LazySingleton.getInstance();
});
lazySingletonFutureList.add(future);
}
countDownLatch1.await();
// 对象地址打印
print(lazySingletonFutureList);
// 3:内部类
System.out.println("内部类地址打印");
List<Future<StaticNestSingleton>> staticNestSingletonFutureList = new ArrayList<>();
CountDownLatch countDownLatch2 = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Future<StaticNestSingleton> future = executorService.submit(() -> {
countDownLatch2.countDown();
return StaticNestSingleton.getInstance();
});
staticNestSingletonFutureList.add(future);
}
countDownLatch2.await();
// 对象地址打印
print(staticNestSingletonFutureList);
// 4:枚举
System.out.println("枚举相等判断");
List<Future<EnumSingleton>> enumSingletonFutureList = new ArrayList<>();
CountDownLatch countDownLatch3 = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Future<EnumSingleton> future = executorService.submit(() -> {
countDownLatch3.countDown();
return EnumSingleton.INSTANCE;
});
enumSingletonFutureList.add(future);
}
countDownLatch3.await();
// 对象地址判断
System.out.println((enumSingletonFutureList.get(0).get()==enumSingletonFutureList.get(1).get())&&
(enumSingletonFutureList.get(0).get()==enumSingletonFutureList.get(1).get()) &&
(enumSingletonFutureList.get(1).get()==enumSingletonFutureList.get(2).get()) &&
(enumSingletonFutureList.get(2).get()==enumSingletonFutureList.get(3).get()) &&
(enumSingletonFutureList.get(3).get()==enumSingletonFutureList.get(4).get()));
}
/**
* 打印对象
* @param list
* @param <T>
*/
<T> void print(List<Future<T>> list){
list.forEach(s ->{
try {
System.out.println(s.get().toString());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
打印结果
饿汉式地址打印
com.hello.designPattern.SingletonPattern.EagerSingleton@1e81f4dc
com.hello.designPattern.SingletonPattern.EagerSingleton@1e81f4dc
com.hello.designPattern.SingletonPattern.EagerSingleton@1e81f4dc
com.hello.designPattern.SingletonPattern.EagerSingleton@1e81f4dc
com.hello.designPattern.SingletonPattern.EagerSingleton@1e81f4dc
懒汉式地址打印
com.hello.designPattern.SingletonPattern.LazySingleton@48cf768c
com.hello.designPattern.SingletonPattern.LazySingleton@48cf768c
com.hello.designPattern.SingletonPattern.LazySingleton@48cf768c
com.hello.designPattern.SingletonPattern.LazySingleton@48cf768c
com.hello.designPattern.SingletonPattern.LazySingleton@48cf768c
内部类地址打印
com.hello.designPattern.SingletonPattern.StaticNestSingleton@4aa8f0b4
com.hello.designPattern.SingletonPattern.StaticNestSingleton@4aa8f0b4
com.hello.designPattern.SingletonPattern.StaticNestSingleton@4aa8f0b4
com.hello.designPattern.SingletonPattern.StaticNestSingleton@4aa8f0b4
com.hello.designPattern.SingletonPattern.StaticNestSingleton@4aa8f0b4
枚举相等判断
true
从结果中可以看到,并发情况下,上面各个类型的单例都可以返回相同的对象实例。总而言之,并发对文中编写的单例示例无破坏。接下来我们来看下反射对单例是否有破坏
反射破坏
测试用例
/**
* 反射单例破坏
* https://blog.csdn.net/liuyingming910/article/details/42457265
* @author linmeng
* @date 2022年3月29日 11:13
* @return void
*/
@Test
public void reflectDestroy() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println("饿汉式单例反射测试");
EagerSingleton eagerSingleton1 = EagerSingleton.getInstance();
Constructor<EagerSingleton> declaredConstructor = EagerSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EagerSingleton eagerSingleton = declaredConstructor.newInstance();
System.out.println(eagerSingleton);
System.out.println(eagerSingleton1);
System.out.println(eagerSingleton == eagerSingleton1);
System.out.println("懒汉式单例反射测试");
LazySingleton lazySingleton1 = LazySingleton.getInstance();
Constructor<LazySingleton> lazySingletonConstructor = LazySingleton.class.getDeclaredConstructor();
lazySingletonConstructor.setAccessible(true);
LazySingleton lazySingleton = lazySingletonConstructor.newInstance();
System.out.println(lazySingleton1);
System.out.println(lazySingleton);
System.out.println(lazySingleton1 == lazySingleton);
System.out.println("内部类单例测试");
Class<StaticNestSingleton> staticNestSingletonClass = StaticNestSingleton.class;
Constructor<StaticNestSingleton> staticNestSingletonConstructor = staticNestSingletonClass.getDeclaredConstructor();
staticNestSingletonConstructor.setAccessible(true);
StaticNestSingleton staticNestSingleton = staticNestSingletonConstructor.newInstance();
StaticNestSingleton staticNestSingleton1 = StaticNestSingleton.getInstance();
System.out.println(staticNestSingleton);
System.out.println(staticNestSingleton1);
System.out.println(staticNestSingleton1 == staticNestSingleton);
System.out.println("枚举单例测试");
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
Constructor<?>[] declaredConstructors = EnumSingleton.class.getDeclaredConstructors();
for (Constructor<?> constructor : declaredConstructors) {
System.out.println(constructor);
}
Constructor<?> enumSingletonConstructor = declaredConstructors[0];
// System.out.println(enumSingletonConstructor);
enumSingletonConstructor.setAccessible(true);
Object enumSingleton1 = enumSingletonConstructor.newInstance("INSTANCE",0);
System.out.println(enumSingleton == enumSingleton1);
}
打印结果
饿汉式单例反射测试
com.hello.designPattern.SingletonPattern.EagerSingleton@78e03bb5
com.hello.designPattern.SingletonPattern.EagerSingleton@5e8c92f4
false
懒汉式单例反射测试
com.hello.designPattern.SingletonPattern.LazySingleton@61e4705b
com.hello.designPattern.SingletonPattern.LazySingleton@50134894
false
内部类单例测试
com.hello.designPattern.SingletonPattern.StaticNestSingleton@2957fcb0
com.hello.designPattern.SingletonPattern.StaticNestSingleton@1376c05c
false
枚举单例测试
枚举单例测试
private com.hello.designPattern.SingletonPattern.EnumSingleton(java.lang.String,int)
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.hello.designPattern.singletonPattern.SingletonTest.reflectDestroy(SingletonTest.java:146)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
不管是饿汉式、懒汉式还是静态内部类,反射都能够破坏其单例对象的唯一性,只有枚举式解决了反射破坏。
序列化破坏
这里我使用了common-lang3提供的序列化工具类SerializationUtils,依赖如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
测试用例
@Test
public void serializeDestroy(){
System.out.println("饿汉式模式");
EagerSingleton eagerSingleton = EagerSingleton.getInstance();
EagerSingleton deserializeEagerSingleton = serializeAndDeserialize(eagerSingleton);
System.out.println(eagerSingleton);
System.out.println(deserializeEagerSingleton);
System.out.println("懒汉式模式");
LazySingleton lazySingleton = LazySingleton.getInstance();
LazySingleton deserializeLazySingleton = serializeAndDeserialize(lazySingleton);
System.out.println(lazySingleton);
System.out.println(deserializeLazySingleton);
System.out.println("内部类模式");
StaticNestSingleton staticNestSingleton = StaticNestSingleton.getInstance();
StaticNestSingleton serializeAndDeserializeStaticNestSingleton = serializeAndDeserialize(staticNestSingleton);
System.out.println(staticNestSingleton);
System.out.println(serializeAndDeserializeStaticNestSingleton);
System.out.println("枚举式单例");
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
EnumSingleton serializeAndDeserializeEnumSingleton = serializeAndDeserialize(enumSingleton);
System.out.println(enumSingleton == serializeAndDeserializeEnumSingleton);
}
private <T extends Serializable> T serializeAndDeserialize(T singleton) {
byte[] eagerSingletonSerializeBytes = SerializationUtils.serialize(singleton);
return SerializationUtils.deserialize(eagerSingletonSerializeBytes);
}
打印结果
饿汉式模式
com.hello.designPattern.SingletonPattern.EagerSingleton@65ae6ba4
com.hello.designPattern.SingletonPattern.EagerSingleton@3b192d32
懒汉式模式
com.hello.designPattern.SingletonPattern.LazySingleton@ed17bee
com.hello.designPattern.SingletonPattern.LazySingleton@2a33fae0
内部类模式
com.hello.designPattern.SingletonPattern.StaticNestSingleton@21588809
com.hello.designPattern.SingletonPattern.StaticNestSingleton@2aae9190
枚举式单例
true
同反射破坏一样,也是只有枚举式单例不被破坏
枚举类单例安全原因
在代码层面上,我没有对枚举式单例做任何处理,那他不被破坏的原因肯定是jdk的内部机制。
并发安全原因
对枚举式单例class文件进行反编译,反编译后代码如下
// javap -c EnumSingleton.class
public final class com.hello.designPattern.SingletonPattern.EnumSingleton extends java.lang.Enum<com.hello.designPattern.SingletonPattern.EnumSingleton> {
public static final com.hello.designPattern.SingletonPattern.EnumSingleton INSTANCE;
public static com.hello.designPattern.SingletonPattern.EnumSingleton[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/hello/designPattern/SingletonPattern/EnumSingleton;
3: invokevirtual #2 // Method "[Lcom/hello/designPattern/SingletonPattern/EnumSingleton;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/hello/designPattern/SingletonPattern/EnumSingleton;"
9: areturn
public static com.hello.designPattern.SingletonPattern.EnumSingleton valueOf(java.lang.String);
Code:
0: ldc #4 // class com/hello/designPattern/SingletonPattern/EnumSingleton
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/hello/designPattern/SingletonPattern/EnumSingleton
9: areturn
public void doSomething();
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String do something
5: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
static {};
Code:
0: new #4 // class com/hello/designPattern/SingletonPattern/EnumSingleton
3: dup
4: ldc #10 // String INSTANCE
6: iconst_0
7: invokespecial #11 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #12 // Field INSTANCE:Lcom/hello/designPattern/SingletonPattern/EnumSingleton;
13: iconst_1
14: anewarray #4 // class com/hello/designPattern/SingletonPattern/EnumSingleton
17: dup
18: iconst_0
19: getstatic #12 // Field INSTANCE:Lcom/hello/designPattern/SingletonPattern/EnumSingleton;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/hello/designPattern/SingletonPattern/EnumSingleton;
26: return
}
说实话,反编译后的代码,我也看不太懂,不过关键字还是可以看懂的,变量INSTANCE被static final修饰,static关键字使得其在类加载时就被初始化,final关键字使得其不可被修改,保证并发安全。
反射安全原因
反射获取对象时是使用的是enumSingletonConstructor.newInstance(“INSTANCE”,0)方法,我们可以看下方法源码,只截取判断的部分
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
获取实例前会判断class对象是不是枚举,如果是枚举的话,直接抛异常。
序列化安全原因
枚举的反序列化使用的是Enum中的valueOf方法获取的对象,实际上还是用的反射获取所有的枚举对象,然后获取对应的枚举实例
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
// 反射调用values方法获取所有的美剧
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
// 将获取到的所有枚举以名称为key,枚举对象为value放到map中去
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
单例破坏问题解决
反射破坏问题解决
反射获取对象时,是直接调用其私有构造方法创建对象的,我们可以直接在构造方法中判断对象是否为空,如果不为空直接抛异常。静态内部类这个不知道怎么弄,有知道的小伙伴可以告诉一下,跪求😃。
// 饿汉式
private EagerSingleton() {
if (eagerSingleton !=null){
throw new RuntimeException();
}
}
//懒汉式
private LazySingleton() {
if (lazySingleton != null) {
throw new RuntimeException();
}
}
序列化破坏问题解决
反序列化的时候,会判断类中有没有readResolve方法,如果有的话,直接使用该方法获取返回对象,我们可以在类中自定义该方法。
public Object readResolve(){
return eagerSingleton;
}
public Object readResolve(){
return lazySingleton;
}
public Object readResolve(){
return SingletonHolder.SINGLETON;
}
建议
直接使用枚举式单例,无需任何额外代码处理,简单高效。