细读Java单例模式
Java中的单例模式看似是一个很简单的设计模式,但事实上,我们可以整出各种各样的“幺蛾子”。单例模式有着不同的实现方式,也很难找到完美的方式。今天我就来分享一下,单例模式的几种常用实现模式以及存在的问题。
之前我写过文章讲解单例模式,不过那个是最简单的方式,还漏掉了许多的情况,这里我们就来详细地学习学习,这里还是以“一个店只能有一个老板”为例,创建老板类单例。
1,常规实现方式
(1) 饿汉式
这个就是上一篇博客讲的方法,也是最简单的实现方法:
package com.example.singleinstance.eager;
import lombok.Getter;
import lombok.Setter;
/**
* 饿汉式单例模式
*/
@Getter
@Setter
public class Master {
/**
* 名字
*/
private String name;
/**
* 唯一单例
*/
private static Master instance = new Master();
/**
* 私有化构造器
*/
private Master() {
}
/**
* 获取老板唯一单例
*
* @return 老板唯一单例
*/
public static Master getInstance() {
return instance;
}
}
可见饿汉式单例模式之所以叫饿汉式,是因为这种单例模式在类加载的时候就初始化了唯一单例了。
这种方式的优缺点也很明显:
- 优点:执行效率高,绝对线程安全
- 缺点:有可能用不着该单例,但是它无论如何都初始化了,可能会“占着茅坑不拉屎”,浪费内存
那么如果要改善性能,我们需要进行一些修改。
(2) 懒汉式
懒汉式单例模式就是当外部访问该单例的时候,才会初始化:
package com.example.singleinstance.lazy;
import lombok.Getter;
import lombok.Setter;
/**
* 懒汉式单例模式
*/
@Getter
@Setter
public class Master {
/**
* 名字
*/
private String name;
/**
* 唯一单例,先不初始化
*/
private static Master instance = null;
/**
* 私有化构造器
*/
private Master() {
}
/**
* 获取老板唯一单例
*
* @return 老板唯一单例
*/
public static Master getInstance() {
// 若没有初始化,则初始化一下
if (instance == null) {
instance = new Master();
}
return instance;
}
}
可见我们先不初始化单例,在要调用的时候,判断是否为null
,如果是说明是第一次调用,则初始化一下,否则就返回单例。
2,想办法破解单例模式
(1) 多线程破坏单例模式
懒汉式单例模式确实优化了性能,但是并非是线程安全的。假设有n个线程在极短的时间同时访问该单例的getInstance
方法,那有可能会有多余一个线程同时判断该单例为null
导致最后初始化出多个Master
实例。
我们实例化单例的时候就打印输出一下单例的地址,修改getInstance
如下:
public static Master getInstance() {
// 若没有初始化,则初始化一下
if (instance == null) {
instance = new Master();
System.out.println(instance);
}
return instance;
}
然后新建两个线程,利用IDEA的线程调试模式,干预线程的执行顺序,来模拟出两个线程同时执行到的情况:
for (int i = 0; i < 2; i++) {
new Thread(() -> {
Master master = Master.getInstance();
}).start();
}
在这里打断点,并右键断点-线程模式:
执行调试,可以在调试控制台这里手动切换线程,控制线程运行:
使用步入按钮(F5),先让0号线程进入if
语句,到达实例化这里停下:
切换到1号线程,也让1号线程进入if
语句,到达实例化这里停下:
最后让两个线程执行完,可以看见控制台输出了两个不同的地址:
可见,懒汉式也不完全是线程安全的。
这时,我们可以给getInstance
方法上锁,实现线程安全:
public synchronized static Master getInstance() {
// 若没有初始化,则初始化一下
if (instance == null) {
instance = new Master();
}
return instance;
}
这样,确实是线程安全了,但是总归是上了锁,对程序的性能会有一定的影响,那难道就没有好一点的方法了吗?
我们可以从类的初始化的角度想一下,我们可以借助内部类来解决这些问题。在Java中,内部类是延时加载的,也就是说你用它它就加载,不用就不加载,不受外部类的影响。利用内部类的这个特性,我们是否能够把单例放在内部类里面呢?我们来试一下子:
package com.example.singleinstance.lazy;
import lombok.Getter;
import lombok.Setter;
/**
* 懒汉式内部类法单例模式
*/
@Getter
@Setter
public class Master {
/**
* 名字
*/
private String name;
/**
* 私有化构造器
*/
private Master() {
}
/**
* 获取老板唯一单例,final使得该方法不允许被重写或者重载
*
* @return 老板唯一单例
*/
public static final Master getInstance() {
// 返回结果之前,会先加载内部类
return InnerMaster.INSTANCE;
}
/**
* 老板类的内部类,没有用到它就不会加载
*/
private static class InnerMaster {
private static final Master INSTANCE = new Master();
}
}
这种方式完美地解决了饿汉式单例模式的内存问题,和上锁的性能问题。内部类一定是会在方法调用之前初始化,并且它永远只会初始化一次(一个类无法被加载多次),因此避免了线程安全问题。
(2) 反射破坏单例模式
构造器确实被私有化了,但是利用Java的反射机制,仍然可以访问其构造器:
// 利用反射获取构造方法,并设定可访问
Constructor constructor = Master.class.getDeclaredConstructor();
constructor.setAccessible(true);
Master master1 = (Master) constructor.newInstance();
Master master2 = (Master) constructor.newInstance();
System.out.println(master1 == master2);
可见即使是私有化了构造器,我们仍然还是可以把它new
个两下,得到两个实例,违背了单例模式的基本原则。
解决这个问题也不难,我们在构造器里面做点功夫即可:
package com.example.singleinstance.lazy;
import lombok.Getter;
import lombok.Setter;
/**
* 懒汉式内部类法单例模式
*/
@Getter
@Setter
public class Master {
/**
* 名字
*/
private String name;
/**
* 私有化构造器
*/
private Master() {
if (InnerMaster.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例!");
}
}
/**
* 获取老板唯一单例,final使得该方法不允许被重写或者重载
*
* @return 老板唯一单例
*/
public static final Master getInstance() {
// 返回结果之前,会先加载内部类
return InnerMaster.INSTANCE;
}
/**
* 老板类的内部类,没有用到它就不会加载
*/
private static class InnerMaster {
private static final Master INSTANCE = new Master();
}
}
再次运行上述代码:
好了,到这里我们也更进一步地明白了:一个类被加载时,其内部类不会被加载;而这个类被使用到时,其内部类才会被加载。
这里注意加载和使用的区别。应用程序启动时,每个类都会被加载,而你调用这个类用于实例化或者调用其方法的时候,才叫使用这个类。
(3) 序列化破坏单例模式
有时候我们需要把对象序列化并在网络上传输,然后反序列化。大家都知道,反序列化的对象并非是原有的对象,这也破坏了单例模式的原则。
首先让Master
类使用Serializable
接口,然后作如下测试:
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Master.getInstance());
// 再反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Master master = (Master) ois.readObject();
System.out.println(master == Master.getInstance());
可见,利用序列化法破坏了单例。
其实,我们只需要在Master
类中增加一个readResolve
方法即可:
package com.example.singleinstance.lazy;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* 懒汉式内部类法单例模式
*/
@Getter
@Setter
public class Master implements Serializable {
/**
* 名字
*/
private String name;
/**
* 私有化构造器
*/
private Master() {
if (InnerMaster.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例!");
}
}
/**
* 获取老板唯一单例,final使得该方法不允许被重写或者重载
*
* @return 老板唯一单例
*/
public static final Master getInstance() {
// 返回结果之前,会先加载内部类
return InnerMaster.INSTANCE;
}
private Object readResolve() {
return InnerMaster.INSTANCE;
}
/**
* 老板类的内部类,没有用到它就不会加载
*/
private static class InnerMaster {
private static final Master INSTANCE = new Master();
}
}
再次运行:
这看起来非常神奇:为什么加这个方法就可以了呢?事实上这和ObjectInputStream
类的执行逻辑有关。大家可以去研究一下JDK源码就知道了,再次不再过多赘述。
但事实上,这种方法确实保证只返回了一个单例,但是内存中其实还是有多个单例。
当然,肯定有更好的方法。
3,注册式单例模式
顾名思义,注册式单例模式就是把实例先注册到一个地方,获取的时候根据标识符获取。
通常有下列两种方式实现。
(1) 【推荐】枚举式单例模式
利用枚举实现单例模式,也就是把单例类写成枚举类,我们修改Master
类如下:
package com.example.singleinstance.enumerate;
import lombok.Getter;
import lombok.Setter;
@Getter
public enum Master {
/**
* 老板类唯一单例
*/
INSTANCE;
/**
* 名字
*/
@Setter
private String name;
/**
* 获取老板类唯一实例
*
* @return 老板类唯一实例
*/
public static Master getInstance() {
return INSTANCE;
}
}
大家都知道:枚举类中的每一个枚举相当于就是这个枚举类的实例,并且枚举类中也可以写成员变量和方法。
那枚举类中的枚举是不是单例呢?我们来试一下子。
a. 尝试使用反射破坏
Constructor constructor = Master.class.getDeclaredConstructor();
constructor.setAccessible(true);
Master master = (Master) constructor.newInstance();
结果:
可见反射机制找不到枚举类的构造器,这是因为枚举类的构造方法是protected
的:
b. 尝试使用序列化破坏
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Master.getInstance());
// 再反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Master master = (Master) ois.readObject();
System.out.println(master == Master.getInstance());
结果:
这也是利用JDK的反序列化机制,也就是说枚举类型其实是通过类名和类对象找到一个唯一的对象,不会被类加载器加载多次。
这也可见:枚举值天生就是单例的,非常契合单例模式思想。
(2) 容器式单例
我们还可以使用Map
专门做一个单例容器,把实例都放进去:
package com.example.singleinstance;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 单例容器
*/
public class SingleContainer {
private SingleContainer() {
}
// 存放所有单例的容器,键为类的全限定名,值为对应单实例
private static Map<String, Object> container = new ConcurrentHashMap<>();
/**
* 获取对应类的单实例,不存在则创建
*
* @param className 类的全限定名
* @return 单实例
*/
public synchronized static Object getInstance(String className) throws Exception {
if (!container.containsKey(className)) {
Object instance = Class.forName(className).getConstructor().newInstance();
container.put(className, instance);
return instance;
}
return container.get(className);
}
}
这种方式看起来也很高级,不过也会产生线程问题。
4,总结
可见单例模式看起来简单,事实上要想写一个严谨、滴水不漏的单例模式还是很难的。
日常开发,推荐使用基于内部类的懒汉式单例模式或者是枚举式单例模式。将两者示例代码拎出来如下:
基于内部类的懒汉式单例模式:
package com.example.singleinstance.lazy;
import lombok.Getter;
import lombok.Setter;
/**
* 懒汉式内部类法单例模式
*/
@Getter
@Setter
public class Master {
/**
* 名字
*/
private String name;
/**
* 私有化构造器
*/
private Master() {
if (InnerMaster.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例!");
}
}
/**
* 获取老板唯一单例,final使得该方法不允许被重写或者重载
*
* @return 老板唯一单例
*/
public static final Master getInstance() {
// 返回结果之前,会先加载内部类
return InnerMaster.INSTANCE;
}
private Object readResolve() {
return InnerMaster.INSTANCE;
}
/**
* 老板类的内部类,没有用到它就不会加载
*/
private static class InnerMaster {
private static final Master INSTANCE = new Master();
}
}
枚举式单例模式:
package com.example.singleinstance.enumerate;
import lombok.Getter;
import lombok.Setter;
@Getter
public enum Master {
/**
* 老板类唯一单例
*/
INSTANCE;
/**
* 名字
*/
@Setter
private String name;
/**
* 获取老板类唯一实例
*
* @return 老板类唯一实例
*/
public static Master getInstance() {
return INSTANCE;
}
}
转载自:https://juejin.cn/post/7042664354679881742