内功心法专题-设计模式-3、单例模式
1. 创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:
- 单例模式 (Singleton)
- 工厂方法模式 (FactoryMethod)
- 抽象工程模式 (AbstractFactory)
- 原型模式 (Prototype)
- 建造者模式 (Builder)
❕
2. 是什么
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法 (静态方法)。
比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。
3. 实现方式⭐️🔴
3.1. 八种单例模式
- 饿汉式 (静态常量)
- 饿汉式(静态代码块)
- 懒汉式 (线程不安全)
- 懒汉式 (线程安全,同步方法)
- 懒汉式 (线程安全,同步代码块)
- 双重检查⭐️🔴
- 静态内部类⭐️🔴
- 枚举⭐️🔴
❕
3.2. 饿汉式(静态常量)
3.2.1. 实现方法
- 构造器私有化 (防止 new)
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance
1 |
|
3.2.1.1. 为什么 getInstance 要是 static 的
因为我们要求外部无法 new Singleton 实例对象,所以外部无法使用实例对象调用非静态方法,设置为 static 时,外部才能调用 getInstance 方法来获取我们创建的单实例
3.2.1.2. 为什么 instance 要是 static 的
因为 getInstance 是静态的,所以方法中要访问的变量 instance 必须也是静态的
3.2.2. 优缺点
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
- 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于 classloder 机制避免了多线程的同步问题。不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getlnstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 Lazy loading 的效果
- 结论:这种单例模式可用,可能造成内存浪费
温故知新:性能调优-基础-6、JVM-类装载子系统
3.2.3. 基于 classloder 机制避免了多线程的同步问题
❕
3.2.3.1. 类加载
ClassLoader 的 loadClass 方法使用了 synchronized 加锁
3.2.3.2. 创建对象
虚拟机使用了TLAB 和 CAS方式保证并发安全问题
3.3. 饿汉式(静态代码块)
3.3.1. 实现方法
- 构造器私有化
- 类的内部声明对象
- 在静态代码块中创建对象
- 向外暴露一个静态的公共方法
1 |
|
3.3.2. 优缺点
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的
- 结论:这种单例模式可用,但是可能造成内存浪费
3.4. 懒汉式(线程不安全)
3.4.1. 实现方法
- 构造器私有化
- 类的内部创建对象
- 向外暴露一个静态的公共方法,当使用到该方法时,才去创建 instance
1 |
|
3.4.2. 优缺点
● 1)起到了 Lazy Loading 的效果,但是只能在单线程下使用
● 2)如果在多线程下,一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
● 3)结论:在实际开发中,不要使用这种方式
3.5. 懒汉式(线程安全,同步方法)
3.5.1. 实现方法
● 1)构造器私有化
● 2)类的内部创建对象
● 3)向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
1 |
|
3.5.2. 优缺点
● 1)解决了线程不安全问题
● 2)效率太低了,每个线程在想获得类的实例时候,执行 getlnstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
● 3)结论:在实际开发中,不推荐使用这种方式
3.6. ✅双重检查⭐️🔴
3.6.1. 实现方法
● 1)构造器私有化
● 2)类的内部创建对象,同时用volatile 关键字修饰修饰
● 3)向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题
1 |
|
3.6.2. 优缺点
● 1)Double-Check 概念是多线程开发中常使用到的,我们进行了两次检查,这样就可以保证线程安全了
● 2)这样实例化代码只用执行一次,后面再次访问时直接 return 实例化对象,也避免的反复进行方法同步
● 3)线程安全;延迟加载;效率较高
● 4)结论:在实际开发中,推荐使用这种单例设计模式
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
3.6.3. JDK 源码⭐️🔴
3.6.3.1. LifecycleMetadata
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/spring-framework/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java]]
3.6.3.2. getSingleton
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/spring-framework/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java]]
3.7. ✅静态内部类⭐️🔴
3.7.1. 实现方法
● 1)构造器私有化
● 2)定义一个静态内部类,内部定义当前类的静态属性
● 3)向外暴露一个静态的公共方法
1 |
|
3.7.2. 优缺点
● 1)这种方式采用了类装载的机制,来保证初始化实例时只有一个线程
● 2)静态内部类方式在 Singleton 类被装载时并不会立即进行类加载,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性,然后后面又跟了 new 关键字,所以还会实例化一个对象出来。
● 3)类的静态属性只会在第一次加载类的时候初始化,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
● 4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
● 5)结论:推荐使用
静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
❕
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
3.7.3. 证明静态内部类加载和初始化⭐️🔴
❕
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns/src/main/java/com/itheima/pattern/singleton/demo5/Singleton.java]]
- 为了验证,故意把构造函数改为 public,然后把 Client 中使用的地方注释掉
- 设置 JVM 参数为
-XX:+TraceClassLoading
运行之后搜索 SingletonHolder
并未看到加载信息
- 再把使用的地方解开注释,再次运行,发现加载了静态内部类
至此可以说明,静态内部类在父类加载时不会导致静态内部类的加载,除非静态内部类的属性或方法被使用时,才会触发静态内部类的加载,同时会触发静态内部类的初始化。
3.8. 枚举⭐️🔴
3.8.1. 实现方法
1 |
|
3.8.2. 优缺点
● 1)这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
● 2)这种方式是 Effective Java 作者 Josh Bloch 提倡的方式
● 3)结论:推荐使用
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
枚举属于饿汉式单例模式
3.8.3. 为什么枚举是线程安全的⭐️🔴
❕ ^bbu2xw关键字和接口-6、enum
4. 总结
5. 破坏单例模式的情况⭐️🔴
❕ ^xk9ru2
5.1. 多线程破坏单例
在多线程环境下,线程的时间片是由 CPU 自由分配的,具有随机性,而单例对象作为共享资源可能会同时被多个线程同时操作,从而导致同时创建多个对象。当然,这种情况只出现在懒汉式单例中。如果是饿汉式单例,在线程启动前就被初始化了,不存在线程再创建对象的情况。
如果懒汉式单例出现多线程破坏的情况,我给出以下两种解决方案:
1、改为 DCL 双重检查锁的写法。
2、使用静态内部类的写法,性能更高。
5.2. 指令重排破坏单例
看似简单的一段赋值语句:instance = new Singleton(); 其实 JVM 内部已经被转换为多条执行指令: memory = allocate(); 分配对象的内存空间指令 ctorInstance(memory); 初始化对象 instance = memory; 将已分配存地址赋值给对象引用 1、分配对象的内存空间指令,调用 allocate() 方法分配内存。 2、调用 ctorInstance() 方法初始化对象 3、将已分配存地址赋值给对象引用但是经过重排序后,执行顺序可能是这样的:
memory = allocate(); 分配对象的内存空间指令 instance = memory; 将已分配存地址赋值给对象引用 ctorInstance(memory); 初始化对象 1、分配对象的内存空间指令 2、设置 instance 指向刚分配的内存地址 3、初始化对象 我们可以看到指令重排之后,instance 指向分配好的内存放在了前面,而这段内存的初 始化的指令被排在了后面,在线程 T1 初始化完成这段内存之前,线程 T2 虽然进不去 同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程 T2 获得 instance 对象,如果直接使用就可能发生错误。 如果出现这种情况,我该如何解决呢?只需要在成员变量前加 volatile,保证所有线程 的可见性就可以了。 private static volatile Singleton instance = null;
5.3. 克隆破坏单例
在 Java 中,所有的类就继承自 Object,也就是说所有的类都实现了 clone() 方法。如果是深 clone(),每次都会重新创建新的实例。那如果我们定义的是单例对象,岂不是也可调用 clone() 方法来反复创建新的实例呢?确实,这种情况是有可能发生的。为了避免发生这样结果,我们可以在单例对象中重写 clone() 方法,将单例自身的引用作为返回值。这样,就能避免这种情况发生。
5.4. 反序列化破坏单例
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns/src/main/java/com/itheima/pattern/singleton/demo7/Test.java]]
5.4.1. 为什么会破坏单例模式
[[(210条消息) 为什么序列化会破坏单例?_李昊轩的博客的博客-CSDN博客]]
5.4.2. 解决方案
1 |
|
5.4.2.1. 为什么 readResolve 可以解决⭐️🔴
^xpn17m
❕ ^hv1if6
1 |
|
❕
通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给 rep 变量
这样多次调用 ObjectInputStream 类中的 readObject 方法,继而就会调用我们定义的 readResolve 方法,所以返回的是同一个对象。
5.5. 反射破坏单例
[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns/src/main/java/com/itheima/pattern/singleton/demo8/Client.java]]
5.5.1. 解决方案
在私有的构造方法中增加判断
1 |
|
6. 案例分析
6.1. JDK 源码解析 -Runtime 类
Runtime 类就是使用的单例设计模式。
通过源代码查看使用的是哪儿种单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}从上面源代码中可以看出 Runtime 类使用的是恶汉式(静态属性)方式来实现单例模式的。
使用 Runtime 类中的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class RuntimeDemo {
public static void main(String[] args) throws IOException {
//获取Runtime类对象
Runtime runtime = Runtime.getRuntime();
//返回 Java 虚拟机中的内存总量。
System.out.println(runtime.totalMemory());
//返回 Java 虚拟机试图使用的最大内存量。
System.out.println(runtime.maxMemory());
//创建一个新的进程执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");
//获取命令执行后的结果,通过输入流获取
InputStream inputStream = process.getInputStream();
byte[] arr = new byte[1024 * 1024* 100];
int b = inputStream.read(arr);
System.out.println(new String(arr,0,b,"gbk"));
}
}