当前位置:网站首页>初级必备:单例模式的7个问题
初级必备:单例模式的7个问题
2022-08-01 11:55:00 【JackieZhengChina】
故事
实话实说,关于单例模式,网上有N多个版本。你估计也看过很多版本。但看完了又能怎样?我技术群里的一位小伙伴,上周面试,就因为一个单例模式,然后叫他回去等通知了。
下面是这位同学被问到的问题:
1、说说单例模式的特点?
2、你知道单例模式的具体使用场景吗?
3、单例模式常见写法有几种?
4、怎么样保证线程安全?
5、怎么不会被反射攻击?
6、怎样保证不会被序列化和反序列化的攻击?
7、枚举为什么会不会被序列化?
.....
你也可以尝试行的回答这几个题,看看自己能回答上几个。
定义
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例
4、隐藏所有的构造方法
**目的:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
案例:一家企业只能有一个CEO,有多个了其实乱套了。
使用场景
需要确保任何情况下都绝对只有一个实例。
比如:ServletContext、ServletConfig、ApplicationContext、DBTool等,都使用到了单列模式。
单例模式的写法
饿汉式
懒汉式(包含双重检查锁、静态内部类)
注册式(以枚举为例)
饿汉式
从名字上就能看出,饿汉:饿了就得先吃饱,所以,一开始就搞定了。
饿汉式主要是使用了static,饿汉式也有两种写法,但本质可以理解为是一样的。
public class HungrySingleton{
private static final HungrySingleton INSTANCE;
static {
INSTANCE=new HungrySingleton();
}
// private static final HungrySingleton INSTANCE=new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
饿汉式有个致命的缺点:浪费空间,不需要也实例化。如果是成千上万个,也这么玩,想想有多恐怖。
于是,就会想到,能不能在使用的时候在实例化,从而引出了懒汉式。
懒汉式
顾名思义,就是需要的时候再创建,因为懒,你不调用我方法,我是不会干活的。
下面是懒汉式的Java代码实现:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {//01
lazySingleton = new LazySingleton();//02
}
return lazySingleton;
}
}
进入getInstance方法,先判断lazySingleton是否为空,为空,则创建一个对象,然后返回此对象。
但是,问题来了:
两个线程同时进入getInstance方法,然后都去执行01这行代码,都是true,然后各自进去创建一个对象,然后返回自己创建的对象。
这岂不是不满足只有唯一 一个对象的了吗?所以这类存在线程安全的问题,那怎么解决呢?
第一印象肯定都是想到加锁。于是,就有了下面的线程安全的懒加载版本:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
//简单粗暴的线程安全问题解决方案
//依然存在性能问题
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
给getInstance方法加锁同步锁标志synchronized,但是又涉及到锁的问题了,同步锁是对系统性能优影响的,尽管JDK1.6后,对其做了优化,但它毕竟还是涉及到锁的开销。
每个线程调用getInstance方法时候,都会涉及到锁,所以又对此进行了优化成为了大家耳熟能详的双重检查锁。
双重检查锁
代码实现如下:
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {//01
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {//02
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
这段代码中,在01行,如果不为空,就直接返回,这是第一次检查。如果为空,则进入同步代码块,02行又进行一次检查。
双重检查就是现实if判断、获取类对象锁、if判断。
上面这段代码,看似没问题,其实还是有问题的,比如:指令重排序(需要有JVM知识垫底哈)
指令重排是什么意思呢?
比如java中简单的一句
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
为了防止指令重排序,所以,我们可以使用volatile来做文章(注意:volatile能防止指令重排序和线程可见性)。
于是,更好的版本就出来了。
public class LazyDoubleCheckSingleton {
//使用volatile修饰
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
尽管相比前面的版本,确实改进了很多,但依然有同步锁,还是会影响性能问题。于是,又进行优化为静态内部类方式:
静态内部类
下面是静态内部类的代码实现:
public class LazyStaticSingleton {
private LazyStaticSingleton() {
}
public static LazyStaticSingleton getInstance() {
return LazyHolder.LAZY_STATIC_SINGLETON;
}
//需要等到外部方法调用是猜执行
//巧用内部类的特性
//JVM底层执行,完美的规避了线程安全的问题
private static class LazyHolder {
private static final LazyStaticSingleton LAZY_STATIC_SINGLETON = new LazyStaticSingleton();
}
}
利用了内部类的特性,在JVM底层,能完美的规避了线程安全的问题,这种方式也是目前很多项目里喜欢使用的方式。
但是,还是会存在潜在的风险,什么风险呢?
可以使用 反射 暴力的串改,同样也会出现创建多个实例:
反射代码实现如下:
import java.lang.reflect.Constructor;
public class LazyStaticSingletonTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyStaticSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance();
Object object1 = LazyStaticSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
这段代码运行结果为false。
所以,上面说的双重检查锁的方式,通过反射,还是会存在潜在的风险。怎么办呢?
在《Effect java 》这本书中,作者推荐使用枚举来实现单例模式,因为枚举不能被反射。
枚举
下面是枚举式的单例模式的代码实现:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
我们把上面反射的那个代码,来测试这个枚举式单例模式。
public class EnumTest {
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance();
Object object1 = EnumSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
运行这段代码:
java.lang.NoSuchMethodException: com.tian.my_code.test.designpattern.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:41)
还真的不能用反射来搞。如果此时面试官,为什么枚举不能被反射呢?
为什么枚举不能被反射呢?
我们在反射的代码中
Constructor constructor = clazz.getDeclaredConstructor(null);
这行代码是获取他的无参构造方法。并且,从错误日志中,我们也可以看到,错误出现就是在getConstructor0方法中,并且,提示的是没有找到无参构造方法。
很奇怪,枚举也是类,不是说如果我们不给类显示定义构造方法时候,会默认给我们创建一个无参构造方法吗?
于是,我想到了一个办法,我们可以使用jad这个工具去反编译的我们的枚举式单例的.class文件。
找到我们的class文件所在目录,然后我们可以执行下面这个命令:
C:\Users\Administrator>jad D:\workspace\my_code\other-local-demo\target\classes
com\tian\my_code\test\designpattern\singleton\EnumSingleton.class
Parsing D:\workspace\my_code\other-local-demo\target\classes\com\tian\my_code\t
st\designpattern\singleton\EnumSingleton.class... Generating EnumSingleton.jad
注意:class文件目录以及生成的jad文件所在的目录。
然后打开EnumSingleton.jad 文件:

于是,我就想到了,那我们使用有参构造方法来创建:
public class EnumTest {
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance("田维常",996);
Object object1 = EnumSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
再次运行这段代码,结果:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:45)
提示很明显了,就是不让我们使用反射的方式创建枚举对象。
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//Modifier.ENUM就是用来判断是否为枚举的
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
所以,到此,我们才算真正的理清楚了,为什么枚举不让反射的原因。
序列化破坏
我们以非线程安全的饿汉式来演示一下,看看序列化是如何破坏到了模式的。
public class ReflectTest {
public static void main(String[] args) {
// 准备两个对象,singleton1接收从输入流中反序列化的实例
HungrySingleton singleton1 = null;
HungrySingleton singleton2 = HungrySingleton.getInstance();
try {
// 序列化
FileOutputStream fos = new FileOutputStream("HungrySingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("HungrySingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (HungrySingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
[email protected]6cbb7a
[email protected]2b3a41
false
看到了吗?
使用序列化是可以破坏到了模式的,这种方式,可能很多人不是很清楚。
如何防止呢?
我们对非线程安全的饿汉式代码进行稍微修改:
public class HungrySingleton implements Serializable{
private static final HungrySingleton INSTANCE;
static {
INSTANCE=new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
//添加了readResolve方法,并返回INSTANCE
private Object readResolve方法,并返回(){
return INSTANCE;
}
}
再次运行上那段序列化测试的代码,其结果如下:
[email protected]2b3a41
[email protected]2b3a41
true
嘿嘿,这样我们是不是就避免了只创建了一个实例?
答案:否
在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))方法。
在readOrdinaryObject方法中有这么一段代码:
Object obj;
try {
//是否有构造方法,有构造放就创建实例
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
}
//判断单例类是否有readResolve方法
if (desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
}
//invokeReadResolve方法中
if (readResolveMethod != null) {
//调用了我们单例类中的readResolve,并返回该方法返回的对象
//注意:是无参方法
return readResolveMethod.invoke(obj, (Object[]) null);
}
绕了半天,原来他是这么玩的,上来就先创建一个实例,然后再去检查我们的单例类是否有readResolve无参方法,我们单例类中的readResolve方法
private Object readResolve(){
return INSTANCE;
}
结论
我们重写了readResolve()无参方法,表面上看是只创建了一个实例,其实只创建了两个实例。
紧接着,面试官继续问:枚举式单例能不能被序列化破坏呢?
枚举式单例能不能被序列化破坏呢?
答案:不能被破坏,请看我慢慢给你道来。
don't talk ,show me the code。
我们先来验证一下是否真的不能被破坏,请看代码:
public class EnumTest {
public static void main(String[] args) {
// 准备两个对象,singleton1接收从输入流中反序列化的实例
EnumSingleton singleton1 = null;
EnumSingleton singleton2 = EnumSingleton.getInstance();
try {
// 序列化
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
INSTANCE
INSTANCE
true
确实,枚举式单例是不会被序列化所破坏,那为什么呢?总得有个证件理由吧。
在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))方法。
case TC_ENUM:
return checkResolve(readEnum(unshared));
在readEnum方法中
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
//重点
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
//...其他代码省略
}
}
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
//enumType.enumConstantDirectory()返回的是一个HashMap
//通过HashMap的get方法获取
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);
}
//返回一个HashMap
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
//使用的是HashMap
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
所以,枚举式单例模式是使用了Map<String, T>,Map的key就是我们枚举类中的INSTANCE。由于Map的key的唯一性,然后就缔造出唯一实例。江湖上也把这个枚举式单例模式叫做注册式单例模式。
在Spring中也是有大量使用这种注册式单例模式,IOC容器就是典型的代表。
总结
本文讲述了单例模式的定义、单例模式常规写法。单例模式线程安全问题的解决,反射破坏、反序列化破坏等。
注意:不要为了套用设计模式,而使用设计模式。而是要,在业务上遇到问题时,很自然地联想单设计模式作为一种捷径方法。
单例模式的优缺点
优点
在内存中只有一个实例,减少内存开销。可以避免对资源的多重占用。设置全局访问点,严格控制访问。
缺点
没有借口,扩展性很差。如果要扩展单例对象,只有修改代码,没有其他途径。
单例模式是 不符合开闭原则的。
知识点
单例模式的重点知识总结:
私有化构造器
保证线程安全
延迟加载
防止反射攻击
防止序列化和反序列化的破坏
边栏推荐
- How much do you know about Amazon reviews?
- 深入理解 Istio —— 云原生服务网格进阶实战
- 如何使用 Authing 单点登录,集成 Discourse 论坛?
- Favorites|Mechanical Engineer Interview Frequently Asked Questions
- Audio and Video Technology Development Weekly | 256
- R语言检验时间序列的平稳性:使用tseries包的adf.test函数实现增强的Dickey-Fuller(ADF)检验、检验时序数据是否具有均值回归特性(平稳性)、具有均值回归特性的案例
- 【CLion】CLion 总是提示 “This file does not belong to any project target xxx” 的解决方法
- Promise learning (4) The ultimate solution for asynchronous programming async + await: write asynchronous code in a synchronous way
- 一篇文章,带你详细了解华为认证体系证书(2)
- [Nodejs] node的fs模块
猜你喜欢
随机推荐
语音聊天app源码——语音聊天派对
如何使用 Authing 单点登录,集成 Discourse 论坛?
Deep understanding of Istio - advanced practice of cloud native service mesh
音视频技术开发周刊 | 256
RK3399 platform development series on introduction to (kernel) 1.52, printk function analysis - the function call will be closed
bat countdown code
[Nodejs] fs module of node
js中常用追加元素的几种方法:append,appendTo,after,before,insertAfter,insertBefore,appendChild
【CLion】CLion 总是提示 “This file does not belong to any project target xxx” 的解决方法
Aeraki Mesh became CNCF sandbox project
Solve vscode input! Unable to quickly generate skeletons (three methods for the new version of vscode to quickly generate skeletons)
Sparse representation - study notes
NIO‘s Sword(思维,取模,推公式)
What is MNIST (what does plist mean)
Favorites|Mechanical Engineer Interview Frequently Asked Questions
MarkDown公式指导手册
activiti工作流的分页查询避坑
JS 中的 undefined 和 null 的区别
Transfer learning to freeze the network:
Promise learning (2) An article takes you to quickly understand the common APIs in Promise









