知识汇总第一篇(单例讲解)

2024-06-14 21:08

本文主要是介绍知识汇总第一篇(单例讲解),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

枚举很适合用来实现单例模式。实际上,在 Effective Java 中也提到过(果然英雄所见略同):

单元素的枚举类型经常成为实现 Singleton 的最佳方法 。

首先什么是单例?就一条基本原则,单例对象的类只会被初始化一次。在 Java 中,我们可以说在 JVM 中只存在该类的唯一一个对象实例。在 Android 中,我们可以说在程序运行期间,该类有且仅有一个对象实例。说到单例模式的实现,你们肯定信手拈来,什么懒汉,饿汉,DCL,静态内部类,门清。在说单例之前,考虑下面几个问题:

  • 你的单例线程安全吗?

  • 你的单例反射安全吗?

  • 你的单例序列化安全吗?

今天,我就来钻钻牛角尖,看看你们的单例是否真的 “单例”。

一、单例的一般实现

1、饿汉式

public class HungrySingleton {private static final HungrySingleton mInstance = new HungrySingleton();private HungrySingleton() {}public static HungrySingleton getInstance() {return mInstance;}
}

私有构造器是单例的一般套路,保证不能在外部新建对象。饿汉式在类加载时期就已经初始化实例,由于类加载过程是线程安全的,所以饿汉式默认也是线程安全的。它的缺点也很明显,我真正需要单例对象的时机是我调用 getInstance() 的时候,而不是类加载时期。如果单例对象是很耗资源的,如数据库,socket 等等,无疑是不合适的。于是就有了懒汉式。

2、懒汉式

public class LazySingleton {private static LazySingleton mInstance;private LazySingleton() {}public static synchronized LazySingleton getInstance() {if (mInstance == null)mInstance = new LazySingleton();return mInstance;}
}

实例化的时机挪到了 getInstance() 方法中,做到了 lazy init ,但也失去了类加载时期初始化的线程安全保障。因此使用了 synchronized 关键字来保障线程安全。但这显然是一个无差别攻击,管你要不要同步,管你是不是多线程,一律给我加锁。这也带来了额外的性能消耗。这点问题肯定难不倒程序员们,于是,双重检查锁定(DCL, Double Check Lock) 应运而生。

3、DCL

public class DCLSingleton {private static DCLSingleton mInstance;private DCLSingleton() {}public static DCLSingleton getInstance() {if (mInstance == null) {                    // 1synchronized (DCLSingleton.class) {     // 2if (mInstance == null)              // 3mInstance = new DCLSingleton(); // 4}}return mInstance;}
}

1 处做第一次判断,如果已经实例化了,直接返回对象,避免无用的同步消耗。2 处仅对实例化过程做同步操作,保证单例。3 处做第二次判断,只有 mInstance 为空时再初始化。看起来时多么的完美,保证线程安全的同时又兼顾性能。但是 DCL 存在一个致命缺陷,就是重排序导致的多线程访问可能获得一个未初始化的对象。

首先记住上面标记的 4 行代码。其中第 4 行代码 mInstance = new DCLSingleton(); 在 JVM 看来有这么几步:

  1. 为对象分配内存空间

  2. 初始化对象

  3. 将 mInstance 引用指向第 1 步中分配的内存地址

在单线程内,在不影响执行结果的前提下,可能存在指令重排序。例如下列代码:

int a = 1;
int b = 2;

在 JVM 中你是无法确保这两行代码谁先执行的,因为谁先执行都不影响程序运行结果。同理,创建实例对象的三部中,第 2 步 初始化对象 和 第 3 步 将 mInstance 引用指向对象的内存地址 之间也是可能存在重排序的。

  1. 为对象分配内存空间

  2. 将 mInstance 引用指向第 1 步中分配的内存地址

  3. 初始化对象

这样的话,就存在这样一种可能。线程 A 按上面重排序之后的指令执行,当执行到第 2 行 将 mInstance 引用指向对象的内存地址 时,线程 B 开始执行了,此时线程 A 已为 mInstance 赋值,线程 B 进行 DCL 的第一次判断 if (mInstance == null) ,结果为 false,直接返回 mInstance 指向的对象,但是由于重排序的缘故,对象其实尚未初始化,这样就出问题了。还挺绕口的,借用 《Java 并发编程艺术》 中的一张表格,会对执行流程更加清晰。

时间线程 A线程 B
t1A1: 分配对象的内存空间 
t2A3: 设置 mInstance 指向内存空间 
t3 B1: 判断 mInstance 是否为空
t4 B2: 由于 mInstance 不为空,线程 B 将访问 mInstance 指向的对象
t5A2: 初始化对象 
t6A3: 访问 mInstance 引用的对象 

A3 和 A2 发生重排序导致线程 B 获取了一个尚未初始化的对象。

说了半天,该怎么改?其实很简单,禁止多线程下的重排序就可以了,只需要用 volatile 关键字修饰 mInstance 。在 JDK 1.5 中,增强了 volatile 的内存语义,对一个volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。volatile 会禁止一些处理器重排序,此时 DCL 就做到了真正的线程安全。

4、静态内部类模式

public class StaticInnerSingleton {private StaticInnerSingleton(){}private static class SingletonHolder{private static final StaticInnerSingleton mInstance=new StaticInnerSingleton();}public static StaticInnerSingleton getInstance(){return SingletonHolder.mInstance;}
}

鉴于 DCL 繁琐的代码,程序员又发明了静态内部类模式,它和饿汉式一样基于类加载时器的线程安全,但是又做到了延迟加载。SingletonHolder 是一个静态内部类,当外部类被加载的时候并不会初始化。当调用 getInstance() 方法时,才会被加载。

枚举单例暂且不提,放在最后再说。先对上面的单例模式做个检测。

二、真的是单例?

还记得开头的提问吗?

  • 你的单例线程安全吗?

  • 你的单例反射安全吗?

  • 你的单例序列化安全吗?

上面大篇幅的论述都在说明线程安全。下面看看反射安全和序列化安全。

1、反射安全

直接上代码,我用 DCL 来做测试:

public static void main(String[] args) {DCLSingleton singleton1 = DCLSingleton.getInstance();DCLSingleton singleton2 = null;try {Class<DCLSingleton> clazz = DCLSingleton.class;Constructor<DCLSingleton> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);singleton2 = constructor.newInstance();} catch (Exception e) {e.printStackTrace();}System.out.println(singleton1.hashCode());System.out.println(singleton2.hashCode());}

执行结果:

1627674070
1360875712

很无情,通过反射破坏了单例。如何保证反射安全呢?只能以暴制暴,当已经存在实例的时候再去调用构造函数直接抛出异常,对构造函数做如下修改:

private DCLSingleton() {if (mInstance!=null)throw new RuntimeException("想反射我,没门!");
}

上面的测试代码会直接抛出异常。

2、序列化安全

将你的单例类实现 Serializable 持久化保存起来,日后再恢复出来,他还是单例吗?

public static void main(String[] args) {DCLSingleton singleton1 = DCLSingleton.getInstance();DCLSingleton singleton2 = null;try {ObjectOutput output=new ObjectOutputStream(new FileOutputStream("singleton.ser"));output.writeObject(singleton1);output.close();ObjectInput input=new ObjectInputStream(new FileInputStream("singleton.ser"));singleton2= (DCLSingleton) input.readObject();} catch (Exception e) {e.printStackTrace();}System.out.println(singleton1.hashCode());System.out.println(singleton2.hashCode());}

执行结果:

644117698
793589513

不堪一击。反序列化时生成了新的实例对象。要修复也很简单,只需要修改反序列化的逻辑就可以了,即重写 readResolve() 方法,使其返回统一实例。

protected Object readResolve() {return getInstance();
}

脆弱不堪的单例模式经过重重考验,进化成了完全体,延迟加载,线程安全,反射安全,序列化安全。全部代码如下:

public class DCLSingleton implements Serializable {private static DCLSingleton mInstance;private DCLSingleton() {if (mInstance!=null)throw new RuntimeException("想反射我,没门!");}public static DCLSingleton getInstance() {if (mInstance == null) {synchronized (DCLSingleton.class) {if (mInstance == null)mInstance = new DCLSingleton();}}return mInstance;}protected Object readResolve() {return getInstance();}
}

三、枚举单例

枚举看到 DCL 就开始嘲笑他了,“你瞅瞅你那是啥,写个单例费那大劲呢?” 于是撸起袖子自己写了一个枚举单例:

public enum EnumSingleton {INSTANCE;
}

DCL 反问,“你这啥玩意,你这就是单例了?我来扒了你的皮看看 !” 于是 DCL 掏出 jad ,扒了 Enum 的衣服,拉出来示众:

public final class EnumSingleton extends Enum {public static EnumSingleton[] values() {return (EnumSingleton[])$VALUES.clone();}public static EnumSingleton valueOf(String s) {return (EnumSingleton)Enum.valueOf(test/singleton/EnumSingleton, s);}private EnumSingleton(String s, int i) {super(s, i);}public static final EnumSingleton INSTANCE;private static final EnumSingleton $VALUES[];static {INSTANCE = new EnumSingleton("INSTANCE", 0);$VALUES = (new EnumSingleton[] {INSTANCE});}
}

我们依次来检查枚举单例的线程安全,反射安全,序列化安全。

首先枚举单例无疑是线程安全的,类似饿汉式,INSTANCE 的初始化放在了 static 静态代码段中,在类加载阶段执行。由此可见,枚举单例并不是延时加载的。

对于反射安全,又要掏出上面的检测代码了,根据 EnumSingleton 的构造器,需要稍微做些改动:

public static void main(String[] args) {EnumSingleton singleton1 = EnumSingleton.INSTANCE;EnumSingleton singleton2 = null;try {Class<EnumSingleton> clazz = EnumSingleton.class;Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);singleton2 = constructor.newInstance("test",1);} catch (Exception e) {e.printStackTrace();}System.out.println(singleton1.hashCode());System.out.println(singleton2.hashCode());}

结果直接报错,错误日志如下:

java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.lang.reflect.Constructor.newInstance(Constructor.java:417)at singleton.SingleTest.main(SingleTest.java:16)

错误发生在 Constructor.newInstance() 方法,又要从源码中找答案了,在 newInstance() 源码中,有这么一句:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");

如果是枚举修饰的,直接抛出异常。和之前的对抗反射的手段一致,压根就不给你反射。所以,枚举单例也是天生反射安全的。

最后枚举单例也是序列化安全的,上篇文章中已经说明过,你可以运行测试代码试试。

看起来枚举单例的确是个不错的选择,代码简单,又能保证绝大多数情况下的单例实例唯一。但是真正在开发中大家好像用的并不多,更多的可能应该是枚举在 Java 1.5 中才添加,大家默认已经习惯了其他的单例实现方式。

四、代码最少的单例?

说到枚举单例代码简单,Kotlin 第一个站出来不服了。我敢说第一,谁敢说第二,给你们献丑了:

object KotlinSingleton { }

jad 反编译一下:

public final class KotlinSingleton {private KotlinSingleton(){}public static final KotlinSingleton INSTANCE;static {KotlinSingleton kotlinsingleton = new KotlinSingleton();INSTANCE = kotlinsingleton;}
}

可以看到,Kotlin 的单例其实也是饿汉式的一种,不钻牛角尖的话,基本可以满足大部分需求。

吹毛求疵的谈了谈单例模式,可以看见要完全的保证单例还是有很多坑点的。在开发中并没有必要钻牛角尖,例如 Kotlin 默认提供的单例实现就是饿汉式而已,其实已经可以满足绝大多数的情况了。

这篇关于知识汇总第一篇(单例讲解)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1061507

相关文章

C++工程编译链接错误汇总VisualStudio

目录 一些小的知识点 make工具 可以使用windows下的事件查看器崩溃的地方 dumpbin工具查看dll是32位还是64位的 _MSC_VER .cc 和.cpp 【VC++目录中的包含目录】 vs 【C/C++常规中的附加包含目录】——头文件所在目录如何怎么添加,添加了以后搜索头文件就会到这些个路径下搜索了 include<> 和 include"" WinMain 和

[职场] 公务员的利弊分析 #知识分享#经验分享#其他

公务员的利弊分析     公务员作为一种稳定的职业选择,一直备受人们的关注。然而,就像任何其他职业一样,公务员职位也有其利与弊。本文将对公务员的利弊进行分析,帮助读者更好地了解这一职业的特点。 利: 1. 稳定的职业:公务员职位通常具有较高的稳定性,一旦进入公务员队伍,往往可以享受到稳定的工作环境和薪资待遇。这对于那些追求稳定的人来说,是一个很大的优势。 2. 薪资福利优厚:公务员的薪资和

springboot家政服务管理平台 LW +PPT+源码+讲解

3系统的可行性研究及需求分析 3.1可行性研究 3.1.1技术可行性分析 经过大学四年的学习,已经掌握了JAVA、Mysql数据库等方面的编程技巧和方法,对于这些技术该有的软硬件配置也是齐全的,能够满足开发的需要。 本家政服务管理平台采用的是Mysql作为数据库,可以绝对地保证用户数据的安全;可以与Mysql数据库进行无缝连接。 所以,家政服务管理平台在技术上是可以实施的。 3.1

【汇总】vivado_zynq学习资料

DMA:https://www.xilinx.com/support/answers/57550.html

基于Java医院药品交易系统详细设计和实现(源码+LW+调试文档+讲解等)

💗博主介绍:✌全网粉丝10W+,CSDN作者、博客专家、全栈领域优质创作者,博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 🌟文末获取源码+数据库🌟 感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人  Java精品实战案例《600套》 2023-2025年最值得选择的Java毕业设计选题大全:1000个热

神经网络第一篇:激活函数是连接感知机和神经网络的桥梁

前面发布的文章介绍了感知机,了解了感知机可以通过叠加层表示复杂的函数。遗憾的是,设定合适的、能符合预期的输入与输出的权重,是由人工进行的。从本章开始,将进入神经网络的学习,首先介绍激活函数,因为它是连接感知机和神经网络的桥梁。如果读者认知阅读了本专题知识,相信你必有收获。 感知机数学表达式的简化 前面我们介绍了用感知机接收两个输入信号的数学表示如下:

【计算机组成原理】部分题目汇总

计算机组成原理 部分题目汇总 一. 简答题 RISC和CICS 简要说明,比较异同 RISC(精简指令集)注重简单快速的指令执行,使用少量通用寄存器,固定长度指令,优化硬件性能,依赖软件(如编译器)来提升效率。 CISC(复杂指令集)包含多样复杂的指令,能一条指令完成多步操作,采用变长指令,减少指令数但可能增加执行时间,倾向于硬件直接支持复杂功能减轻软件负担。 两者均追求高性能,但RISC

PTA基础题考点汇总

一:字符串(数组)的逆序,栈的方法 **字符串数组的逆序 : ** 标准容器库的知识:定义stack容器于字符串:stackv; string s; //这里用到了c++中stl(标准容器库的知识)stack;//用的时候要声明头文件;定义stack容器和string;stack<string>v; string s;了解几个函数,v.top( );//让最后一个元素出栈;(v是定义的

数据结构:二叉树详解 c++信息学奥赛基础知识讲解

目录 一、二叉树的定义 二、二叉树的形态 三、二叉树的性质 四、二叉树的存储 五、二叉树的创建与遍历(递归) 六、二叉树实现 创建二叉树 展示二叉树 1、计算数的高度 2、计算数的叶子数量 3、计算数的宽度 4、层次遍历 5、前序遍历 递归写法 非递归写法 6、中序遍历 递归写法 非递归写法 7、后序遍历 递归写法 非递归写法 8、输出根节点到所有叶

rocketmq问题汇总-如何将特定消息发送至特定queue,消费者从特定queue消费

业务描述 由于业务需要这样一种场景,将消息按照id(业务id)尾号发送到对应的queue中,并启动10个消费者(单jvm,10个消费者组),从对应的queue中集群消费,如下图1所示(假设有两个broker组成的集群):  producer如何实现 producer只需发送消息时调用如下方法即可 /*** 发送有序消息** @param messageMap 消息数据* @param