共计 9659 个字符,预计需要花费 25 分钟才能阅读完成。
一 单例模式概述
(一) 什么是单例模式
单例模式属于 创立型模式 之一,它提供了一种创建对象的最佳形式
在软件工程中,创立型模式是解决对象创立的设计模式,试图依据理论状况应用适合的形式创建对象。根本的对象创立形式可能会导致设计上的问题,或减少设计的复杂度。创立型模式通过以某种形式管制对象的创立来解决问题。
因为咱们平时尽管能够定义一个全局变量使一个对象被拜访,然而它并不能保障你屡次实例化对象,最直观的,屡次创建对象的代价就是耗费性能,导致效率会低一些。单例模式就是用来解决这些问题
顺便提一个很常见的例子:例如在 Win 系的电脑下咱们永远只能关上一个工作管理器,这样能够避免出现一些资源节约,以及多窗口显示数据不统一的问题
定义:单例模式,保障一个类仅有一个实例,并且提供一个拜访它的全局拜访点
(二) 特点
- ① 单例类只能有一个实例对象
- ② 单例类必须本人创立本人的惟一实例
- ③ 单例类必须对外提供一个拜访该实例的办法
(三) 优缺点以及应用场景
(1) 长处
- 提供了对惟一实例的受控拜访
-
保障了内存中只有惟一实例,缩小了内存的开销
- 尤其体现在一些须要屡次创立销毁实例的状况下
-
防止对资源的多重占用
- 比方对文件的写操作
(2) 毛病
- 单例模式中没有形象层,没有接口,不能继承,扩大艰难,扩大须要批改原来的代码,违反了 “开闭准则”
- 单例类的代码个别写在同一个类中,肯定水平上职责过重,违反了 “繁多职责准则”
(3) 利用场景
先说几个大家常见单例的例子:
- Windows 下的工作管理器和回收站,都是典型的单例模式,你能够试一下,没法同时关上两个的哈
-
数据库连接池的设计个别也是单例模式,因为频繁的关上敞开与数据库的连贯,会有不小的效率损耗
- 然而滥用单例也可能带来一些问题,例如导致共享连接池对象的程序过多而呈现连接池溢出
- 网站计数器,通过单例解决同步问题
- 操作系统的文件系统
- Web 利用的配置对象读取,因为配置文件属于共享的资源
- 程序的日志利用,个别也是单例,否则追加内容时,容易出问题
所以,依据一些常见的例子,简略总结一下,什么时候用单例模式呢?
- ① 须要频繁创立销毁实例的
- ② 实例创立时,耗费资源过多,或者耗时较多的,例如数据连贯或者 IO
- ③ 某个类只要求生成一个类的状况,例如生成惟一序列号,或者人的身份证
- ④ 对象须要共享的状况,如 Web 中配置对象
二 实现单例模式
依据单例模式的定义和特点,咱们能够分为三步来实现最根本的单例模式
- ① 构造函数私有化
- ② 在类的外部创立实例
- ③ 提供本类实例的惟一全局拜访点,即提供获取惟一实例的办法
(一) 饿汉式
咱们就依照最根本的这三点来写
public class Hungry {
// 结构器公有,静止内部 new
private Hungry(){}
// 在类的外部创立本人的实例
private static Hungry hungry = new Hungry();
// 获取本类实例的惟一全局拜访点
public static Hungry getHungry(){return hungry;}
}
这种做法一开始就间接创立这个实例,咱们也称为饿汉式单例,然而如果 这个实例始终没有被调用,会造成内存的节约,显然这样做是不适合的
(二) 懒汉式
饿汉式的次要问题在于,一开始就创立实例导致的内存节约问题,那么咱们将创建对象的步骤,挪到具体应用的时候
public class Lazy1 {
// 结构器公有,静止内部 new
private Lazy1(){System.out.println(Thread.currentThread().getName() + "拜访到了");
}
// 定义即可,不真正创立
private static Lazy1 lazy1 = null;
// 获取本类实例的惟一全局拜访点
public static Lazy1 getLazy1(){
// 如果实例不存在则 new 一个新的实例,否则返回现有的实例
if (lazy1 == null) {lazy1 = new Lazy1();
}
return lazy1;
}
public static void main(String[] args) {
// 多线程拜访,看看会有什么问题
for (int i = 0; i < 10; i++) {new Thread(()->{Lazy1.getLazy1();
}).start();}
}
}
例如上述代码,咱们只在刚开始做了一个定义,真正的实例化是在调用 getLazy1() 时被执行
单线程环境下是没有问题的,然而多线程的状况下就会呈现问题,例如上面是我运行后果中的一次:
Thread-0 拜访到了
Thread-4 拜访到了
Thread-1 拜访到了
Thread-3 拜访到了
Thread-2 拜访到了
(三) DCL 懒汉式
(1) 办法上间接加锁
很显然,多线程下的一般懒汉式呈现了问题,这个时候,咱们只须要加一层锁就能够解决
简略的做法就是在办法前加上 synchronized 关键字
public static synchronized Lazy1 getLazy1(){if (lazy1 == null) {lazy1 = new Lazy1();
}
return lazy1;
}
(2) 放大锁的范畴
然而咱们又想放大锁的范畴,毕竟办法上加锁,多线程中效率会低一些,所以只把锁加到须要的代码上
咱们直观的可能会这样写
public static Lazy1 getLazy1(){if (lazy1 == null) {synchronized(Lazy1.class){lazy1 = new Lazy1();
}
}
return lazy1;
}
然而这样还是有问题的
(3) 双重锁定
当线程 A 和 B 同时拜访 getLazy1(),执行到到 if (lazy1 == null)
这句的时候,同时判断出 lazy1 == null,也就同时进入了 if 代码块中,前面因为加了锁,只有一个能先执行实例化的操作,例如 A 先进入,然而 前面的 B 进入后同样也能够创立新的实例,就达不到单例的目标了,不信能够本人试一下
解决的形式就是再进行第二次的判断
// 获取本类实例的惟一全局拜访点
public static Lazy1 getLazy1(){
// 如果实例不存在则 new 一个新的实例,否则返回现有的实例
if (lazy1 == null) {
// 加锁
synchronized(Lazy1.class){
// 第二次判断是否为 null
if (lazy1 == null){lazy1 = new Lazy1();
}
}
}
return lazy1;
}
(4) 指令重排问题
这种在适当地位加锁的形式,尽可能的升高了加锁对于性能的影响,也能达到预期成果
然而这段代码,在肯定条件下还是会有问题,那就是指令重排问题
指令重排序是 JVM 为了优化指令,进步程序运行效率,在不影响单线程程序执行后果的前提下,尽可能地进步并行度。
什么意思呢?
首先要晓得 lazy1 = new Lazy1();
这一步并不是一个原子性操作,也就是说这个操作会分成很多步
- ① 调配对象的内存空间
- ② 执行构造函数,初始化对象
- ③ 指向对象到刚调配的内存空间
然而 JVM 为了效率对这个步骤进行了重排序,例如这样:
- ① 调配对象的内存空间
- ③ 指向对象到刚调配的内存空间,对象还没被初始化
- ② 执行构造函数,初始化对象
依照 ① ③ ② 的程序,当 A 线程执行到 ② 后,B 线程判断 lazy1 != null,然而此时的 lazy1 还没有被初始化,所以会出问题,并且这个过程中 B 基本执行到锁那里,配个表格阐明一下:
Time | ThreadA | ThreadB |
---|---|---|
t1 | A:① 调配对象的内存空间 | |
t2 | A:③ 指向对象到刚调配的内存空间,对象还没被初始化 | |
t3 | B:判断 lazy1 是否为 null | |
t4 | B:判断到 lazy1 != null,返回了一个没被初始化的对象 | |
t5 | A:② 初始化对象 |
解决的办法很简略——在定义时减少 volatile 关键字,防止指令重排
(5) 最终代码
最终代码如下:
public class Lazy1 {
// 结构器公有,静止内部 new
private Lazy1(){System.out.println(Thread.currentThread().getName() + "拜访到了");
}
// 定义即可,不真正创立
private static volatile Lazy1 lazy1 = null;
// 获取本类实例的惟一全局拜访点
public static Lazy1 getLazy1(){
// 如果实例不存在则 new 一个新的实例,否则返回现有的实例
if (lazy1 == null) {
// 加锁
synchronized(Lazy1.class){
// 第二次判断是否为 null
if (lazy1 == null){lazy1 = new Lazy1();
}
}
}
return lazy1;
}
public static void main(String[] args) {
// 多线程拜访,看看会有什么问题
for (int i = 0; i < 10; i++) {new Thread(()->{Lazy1.getLazy1();
}).start();}
}
}
(四) 动态外部类懒汉式
双重锁定算是一种可行不错的形式,而动态外部类就是一种更加好的办法,不仅速度较快,还保障了线程平安,先看代码
public class Lazy2 {
// 结构器公有,静止内部 new
private Lazy2(){System.out.println(Thread.currentThread().getName() + "拜访到了");
}
// 用来获取对象
public static Lazy2 getLazy2(){return InnerClass.lazy2;}
// 创立外部类
public static class InnerClass {
// 创立单例对象
private static Lazy2 lazy2 = new Lazy2();}
public static void main(String[] args) {
// 多线程拜访,看看会有什么问题
for (int i = 0; i < 10; i++) {new Thread(()->{Lazy2.getLazy2();
}).start();}
}
}
下面的代码,首先 InnerClass 是一个外部类,其在初始化时是不会被加载的,当用户执行了 getLazy2() 办法才会加载,同时创立单例对象,所以他也是懒汉式的办法,因为 InnerClass 是一个动态外部类,所以只会被实例化一次,从而达到线程平安,因为并没有加锁,所以性能上也会很快,所以个别是举荐的
(五) 枚举形式
最初举荐一个十分好的形式,那就是枚举单例形式,其不仅简略,且保障了平安,先看一下《Effective Java》中作者的阐明:
这种办法在性能上与私有域办法类似,但更加简洁无偿地提供了序列化机制,相对避免屡次实例化。即便是在面对简单的序列化或者反射攻打的时候。尽管这种办法还没有宽泛采纳,然而单元素的枚举类型常常成为实现 Singleton 的最佳办法,留神,如果 Singleton 必须扩大一个超类,而不是扩大 enum 时则不宜应用这个办法,(尽管能够申明枚举去实现接口)。
节选自《Effective Java》第 3 条:用公有结构器或者枚举类型强化 Singleton 属性
原著:Item3: Enforce the singleton property with a private constructor or an enum
代码就这样,几乎不要太简略,拜访通过 EnumSingle.IDEAL
就能够拜访了
public enum EnumSingle {IDEAL;}
咱们接下来就要给大家演示为什么枚举是一种比拟平安的形式
三 反射毁坏单例模式
(一) 单例是如何被毁坏的
上面用双重锁定的懒汉式单例演示一下,这是咱们原来的写法,new 两个实例进去,输入一下
public class Lazy1 {
// 结构器公有,静止内部 new
private Lazy1(){System.out.println(Thread.currentThread().getName() + "拜访到了");
}
// 定义即可,不真正创立
private static volatile Lazy1 lazy1 = null;
// 获取本类实例的惟一全局拜访点
public static Lazy1 getLazy1(){
// 如果实例不存在则 new 一个新的实例,否则返回现有的实例
if (lazy1 == null) {
// 加锁
synchronized(Lazy1.class){
// 第二次判断是否为 null
if (lazy1 == null){lazy1 = new Lazy1();
}
}
}
return lazy1;
}
public static void main(String[] args) {Lazy1 lazy1 = getLazy1();
Lazy1 lazy2 = getLazy1();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
运行后果:
main 拜访到了
cn.ideal.single.Lazy1@1b6d3586
cn.ideal.single.Lazy1@1b6d3586
能够看到,后果是单例没有问题
(1) 一个一般实例化,一个反射实例化
然而咱们如果通过反射的形式进行实例化类,会有什么问题呢?
public static void main(String[] args) throws Exception {Lazy1 lazy1 = getLazy1();
// 取得其空参结构器
Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
// 使得可操作性该 declaredConstructor 对象
declaredConstructor.setAccessible(true);
// 反射实例化
Lazy1 lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
getDeclaredConstructor() 办法阐明
办法返回一个 Constructor 对象,它反映此 Class 对象所示意的类或接口指定的构造函数。parameterTypesparameter 是确定构造函数的形参类型,在 Class 对象申明程序的数组。
public Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException
运行后果:
main 拜访到了
main 拜访到了
cn.ideal.single.Lazy1@1b6d3586
cn.ideal.single.Lazy1@4554617c
能够看到,单例被毁坏了
解决办法:因为咱们反射走的其无参结构,所以在无参结构中再次进行非 null 判断,加上原来的双重锁定,当初也就有三次判断了
// 结构器公有,静止内部 new
private Lazy1(){synchronized (Lazy1.class){if(lazy1 != null) {throw new RuntimeException("反射毁坏单例异样");
}
}
}
不过后果也没让人悲观,这种测试下,第二次实例化会间接报异样
(2) 两个都是反射实例化
如果两个都是反射实例化进去的,也就是说,基本就不去调用 getLazy1() 办法,那可怎么办?
如下:
public static void main(String[] args) throws Exception {
// 取得其空参结构器
Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
// 使得可操作性该 declaredConstructor 对象
declaredConstructor.setAccessible(true);
// 反射实例化
Lazy1 lazy1 = declaredConstructor.newInstance();
Lazy1 lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
运行后果:
main 拜访到了
main 拜访到了
cn.ideal.single.Lazy1@1b6d3586
cn.ideal.single.Lazy1@4554617c
单例又被毁坏了
解决方案:减少一个标识位,例如下文通过减少一个布尔类型的 ideal 标识,保障只会执行一次,更平安的做法,能够进行加密解决,保障其安全性
// 结构器公有,静止内部 new
private Lazy1(){synchronized (Lazy1.class){if (ideal == false){ideal = true;} else {throw new RuntimeException("反射毁坏单例异样");
}
}
System.out.println(Thread.currentThread().getName() + "拜访到了");
}
这样就没问题了吗,并不是,一旦他人通过一些伎俩失去了这个标识内容,那么他就能够通过批改这个标识持续毁坏单例,代码如下(这个把代码贴全一点,后面都是节选要害的,都能够参考这个)
public class Lazy1 {
private static boolean ideal = false;
// 结构器公有,静止内部 new
private Lazy1(){synchronized (Lazy1.class){if (ideal == false){ideal = true;} else {throw new RuntimeException("反射毁坏单例异样");
}
}
System.out.println(Thread.currentThread().getName() + "拜访到了");
}
// 定义即可,不真正创立
private static volatile Lazy1 lazy1 = null;
// 获取本类实例的惟一全局拜访点
public static Lazy1 getLazy1(){
// 如果实例不存在则 new 一个新的实例,否则返回现有的实例
if (lazy1 == null) {
// 加锁
synchronized(Lazy1.class){
// 第二次判断是否为 null
if (lazy1 == null){lazy1 = new Lazy1();
}
}
}
return lazy1;
}
public static void main(String[] args) throws Exception {Field ideal = Lazy1.class.getDeclaredField("ideal");
ideal.setAccessible(true);
// 取得其空参结构器
Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
// 使得可操作性该 declaredConstructor 对象
declaredConstructor.setAccessible(true);
// 反射实例化
Lazy1 lazy1 = declaredConstructor.newInstance();
ideal.set(lazy1,false);
Lazy1 lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
运行后果:
main 拜访到了
main 拜访到了
cn.ideal.single.Lazy1@4554617c
cn.ideal.single.Lazy1@74a14482
实例化 lazy1 后,其执行了批改 ideal 这个布尔值为 false,从而绕过了判断,再次毁坏了单例
所以,能够得出,这几种形式都是不平安的,都有着被反射毁坏的危险
(二) 枚举类不会被毁坏
下面在解说枚举单例形式的时候就提过《Effective Java》中提到,即便是在面对简单的序列化或者反射攻打的时候,(枚举单例形式)相对避免屡次实例化,上面来看一下是不是这样:
首先说一个前提条件:这是 Constructor 下的 newInstance 办法节选,也就是说遇到枚举时,会报异样,也就是不容许通过反射创立枚举
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
看一下咱们枚举单例类 EnumSingle 生成的字节码文件,能够看到其中有一个无参结构,也就是说,咱们还是只须要拿到 getDeclaredConstructor(null) 就行了
代码如下:
public enum EnumSingle {
IDEAL;
public static void main(String[] args) throws Exception {
EnumSingle ideal1 = EnumSingle.IDEAL;
// 取得其空参结构器
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
// 使得可操作性该 declaredConstructor 对象
declaredConstructor.setAccessible(true);
// 反射实例化
EnumSingle ideal2 = declaredConstructor.newInstance();
System.out.println(ideal1);
System.out.println(ideal2);
}
}
运行后果却是出乎意料:
提醒居然是找不到这个空参???字节码中可是却是存在的啊
Exception in thread "main" java.lang.NoSuchMethodException: cn.ideal.single.EnumSingle.<init>()
本人 javap 反编译一下,能够看到还是有这个空参
换成 jad 再看看(将 jad.exe 放在字节码文件同目录下)
- 执行:
jad -sjava EnumSingle.class
提醒曾经反编译完结:Parsing EnumSingle.class… Generating EnumSingle.java
关上生成的 java 文件,终于发现,原来它是一个带参结构,同时有两个参数,String 和 int
所以上面,咱们只须要批改原来的无参为有参即可:
public enum EnumSingle {
IDEAL;
public static void main(String[] args) throws Exception {
EnumSingle ideal1 = EnumSingle.IDEAL;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
// 使得可操作性该 declaredConstructor 对象
declaredConstructor.setAccessible(true);
// 反射实例化
EnumSingle ideal2 = declaredConstructor.newInstance();
System.out.println(ideal1);
System.out.println(ideal2);
}
}
这样就没问题了,提醒了咱们想要的谬误:Cannot reflectively create enum objects
这也阐明,枚举类的单例模式写法的确不会被反射毁坏!