当前位置:网站首页>初级必备:单例模式的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容器就是典型的代表。
总结
本文讲述了单例模式的定义、单例模式常规写法。单例模式线程安全问题的解决,反射破坏、反序列化破坏等。
注意:不要为了套用设计模式,而使用设计模式。而是要,在业务上遇到问题时,很自然地联想单设计模式作为一种捷径方法。
单例模式的优缺点
优点
在内存中只有一个实例,减少内存开销。可以避免对资源的多重占用。设置全局访问点,严格控制访问。
缺点
没有借口,扩展性很差。如果要扩展单例对象,只有修改代码,没有其他途径。
单例模式是 不符合开闭原则的。
知识点
单例模式的重点知识总结:
私有化构造器
保证线程安全
延迟加载
防止反射攻击
防止序列化和反序列化的破坏
边栏推荐
- 轮询和长轮询的区别
- 通配符SSL证书不支持多域名吗?
- A new generation of ultra-safe cellular batteries, Sihao Airun goes on sale starting at 139,900 yuan
- Audio and Video Technology Development Weekly | 256
- 解决vscode输入! 无法快捷生成骨架(新版vscode快速生成骨架的三种方法)
- C language implementation!20000 in 4 seconds
- pandas connects to the oracle database and pulls the data in the table into the dataframe, filters all the data from the current time (sysdate) to one hour ago (filters the range data of one hour)
- Data frame and remote frame of CAN communication
- redis6 跟着b站尚硅谷学习
- 新一代超安全蜂窝电池, 思皓爱跑上市13.99万元起售
猜你喜欢

【likeshop】回收租凭系统100%开源无加密 商城+回收+租赁

Audio and Video Technology Development Weekly | 256

【公开课预告】:超分辨率技术在视频画质增强领域的研究与应用

2022 Go ecosystem rpc framework Benchmark

mysql进阶(二十二)MySQL错误之Incorrect string value中文字符输入错误问题分析

Promise learning (1) What is Promise?how to use?How to solve callback hell?

CloudCompare&PCL ICP配准(点到面)

Why Metropolis–Hastings Works

如何利用DevExpress控件绘制流程图?看完这篇文章就懂了!

C#/VB.NET 将PPT或PPTX转换为图像
随机推荐
腾讯云原生:Areaki Mesh 在 2022 冬奥会视频直播应用中的服务网格实践
Fault 007: The dexp derivative is inexplicably interrupted
[Open class preview]: Research and application of super-resolution technology in the field of video quality enhancement
浏览器存储
JWT
C#/VB.NET 将PPT或PPTX转换为图像
Hot review last week (7.25 7.31)
Promise学习(二)一篇文章带你快速了解Promise中的常用API
mysql进阶(二十二)MySQL错误之Incorrect string value中文字符输入错误问题分析
[Nodejs] fs module of node
安装apex报错
【Unity3D插件】AVPro Video插件分享《视频播放插件》
The use of Ts - Map type
.NET analyzes the LINQ framework in depth (three: the elegant prelude of LINQ)
【随心笔记】假期快过去了,都干了点什么
redis6 跟着b站尚硅谷学习
迁移学习冻结网络的方法:
.NET性能优化-使用SourceGenerator-Logger记录日志
CloudCompare & PCL ICP registration (point to face)
Data frame and remote frame of CAN communication