Java 设计模式之单例模式
Java 设计模式之静态工厂方法模式
Java 设计模式之工厂方法模式
Java 设计模式之抽象工厂模式
Java 设计模式之Builder模式
Java 设计模式之静态工厂、工厂方法、抽象工厂和 Builder 模式的区别
Java 设计模式之代理模式
Java 设计模式之外观模式
“设计模式的重要性不用我多说,如果你想成为优秀的 Android 工程师,设计模式是必须要掌握的。” —— 《 Android 进阶之光 》 刘望舒
单例模式,称得上最简单、最常见的设计模式了,当我打算从单例模式开始写几篇设计模式的文章时,我是有些犹豫的,“这么简单的设计模式,人人都会,还有必要写吗?”
后来,我问了自己三个问题:
1、单例模式共有 7 种实现形式,我能顺畅的说出来吗?
2、7 种实现形式的区别,以及为什么有这些区别,我能可浅可深的说出来吗?
3、7 种实现形式,手写代码的话,我能够写出来吗?
答案是否定的,眼高手低要不得啊!
与大家共勉。
单例模式用来保证一个类只有一个实例,自行实例化此实例,并提供一个访问此实例的全局访问点。
先贴代码:
public class Singleton {
private static Singleton instance = new Singleton();
// 私有构造,不允许外部通过构造实例化 Singleton.class
private Singleton() {
}
public static Singleton newInstance() {
return instance;
}
}
这种写法被称为“饿汉模式”,可我感觉,完全饿不到!
我们都知道,类的加载是由 Java 虚拟机在初始化应用程序时完成的,而类加载时,会立刻执行静态代码。也就是说,只要 Java 虚拟机启动了应用程序,Singleton 类就会被实例化,不管 Singleton 类会不会被用到 —— 不管你吃不吃,饭都在那里。
所以说,我们可以总结出来,饿汉模式的单例模式有以下几个特征:
1、类加载时还要进行实例化,导致类加载速度变慢,但获取对象时速度很快;
2、没有实现懒加载,如果从始至终未使用此类,却默认进行实例化,会造成内存的浪费;
3、由于类的加载是在 Java 虚拟机初始化应用程序时完成的,这时候应用程序实际还未启动,且一个类只会加载一次,避免了多线程的同步问题。
后续的各种单例模式的写法,都是基于饿汉模式发展而来,大家理解了饿汉模式的上述特征的话,后续的各种写法的理解就是水到渠成了。
先贴代码:
public class Singleton {
private static Singleton instance;
// 私有构造,不允许外部通过构造实例化 Singleton.class
private Singleton() {
}
public static Singleton newInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
大家可以很容易的发现,与饿汉模式相比,懒汉模式最大的改进是实现了懒加载。也就是说,此类不用就不加载,用的话在第一次调用时会进行实例化。所以说,可以节省内存资源,但第一次调用时速度会慢一些。
另外,大家还要注意的是,在多线程的情况下,此种写法有可能产生两种问题:
1、创建了多个实例;
2、多线程的安全性问题,可能导致严重的后果,至于问题的原因,留在后文讲“DCL失效问题”时一起详解。
众所周知,移动端的应用程序高并发的情况较少,所以这种写法的单例模式实际上是可以用的,只是确实没有后续的写法优秀。
既然上一种写法的懒汉模式最大的问题是线程不安全,那咱们就来解决下这个问题。
public class Singleton {
private static Singleton instance;
// 私有构造,不允许外部通过构造实例化 Singleton.class
private Singleton() {
}
public static synchronized Singleton newInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
很简单,给 newInstance()
方法加了同步锁 synchronized
。
然而事实是,这种写法是最不建议的用法。因为无论是不是多线程的环境,每次调用 newInstance()
方法都要进行同步,造成了不必要的同步开销。
这种写法就很优秀了,不过也存在一些问题,先看代码吧。
public class Singleton {
private static Singleton instance;
// 私有构造,不允许外部通过构造实例化 Singleton.class
private Singleton() {
}
public static Singleton newInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
咱们看这段代码 12 ~ 14 行,自然会为了实现懒加载;
而代码 11 行,则是为了解决懒加载的线程不安全的问题;
亮点在代码 10 行,在进行同步前,先判空,如果类已经实例化,则不再进行同步,解决了每次调用 newInstance()
方法都要同步造成的不必要的同步开销问题。
综上,DCL 的写法一下子解决了上述 3 种单例模式写法的所有问题。
那 DCL 的写法存在的问题是什么呢?
DCL 失效问题(划重点)!
看代码 13 行 instance = new Singleton()
,我们都知道,这不是一条原子操作,拆分来看的话,大致分为三个步骤:
1、给 Singleton 的实例分配内存空间;
2、调用 Singleton 的构造函数,初始化其成员变量;
3、将 instance 对象指向分配的内存空间(此时,instance 就不是 null 了)。
由于 Java 编译器允许处理器乱序执行(原因一),以及在 JDK 1.5 之前,JMM 中 Cache、寄存器到主内存回写顺序的规定(原因二),导致上述 2、3 步骤顺序无法保证,也就是说执行的顺序可能是 1-2-3,或者是 1-3-2。
1-2-3 的执行顺序没什么问题,我们来关注下 1-3-2 的执行顺序。
假设线程 A 正在对 Singleton 进行实例化,执行了步骤 1 和 3,还没有执行 2。这时候线程 B 进来了,判断 instance 不是 null,就取走了 instance 进行使用,可是 Singleton 的构造函数都还没执行,成员变量也没有初始化,就出错了。
这就是 DCL 的失效问题。
那怎么解决 DCL 的失效问题呢?
在 JDK 1.5 及 1.5 以前,无解;
在 JDK 1.6 及 1.6 以后,JVM 进行了调整,具体化了关键字 volatile
,我们可以在上述代码第 3 行 声明 instance 属性时加入此关键字 private static volatile Singleton instance
,就可以保证 instance 对象每次都是从主内存中读取到的。
我们来简单解释一下:
在 JMM 中,有主内存与工作内存的区分,一般声明的变量会在主内存中存在,并在工作内存中存在其一个拷贝。当工作内存对变量的拷贝进行修改时,会从工作内存同步到主内存。可是当对变量进行非原子性操作时,变量从工作内存到主内存的同步也是非原子性操作,在多线程的情况下,如果存与取同时发生,就会出现线程不安全的问题。
而 volatile
关键字,具备 “有序性” 和 “可见性” 的特性。
“有序性”,禁止指令重排序,解决导致 DCL 失效的原因一;
“可见性”,当某一工作内存对变量的拷贝进行修改时,会立即同步到主内存,同时将所有工作内存中的变量的拷贝置为无效状态,则工作内存要取用此变量就要从主内存中同步过来,解决导致 DCL 失效的原因二。
这样, volatile
关键字基本可以解决 DCL 失效的问题。(JDK <= 1.5 很少用了,可以忽略)
我们知道,使用同步锁 synchronized
进行同步操作是比较耗费资源的,volatile
要比 synchronized
节俭一些,不过还是对性能有所损耗,那有没有更优秀的单例模式写法呢?有的,往下看吧!
这种写法可以说一枝独秀了,先看下代码吧。
public class Singleton{
private Singleton(){
}
public static Singleton newInstance(){
return SingletonHelper.instance;
}
private static class SingletonHelper{
private final static Singleton instance = new Singleton();
}
}
要搞懂这种模式,首先要搞懂 “饿汉模式”,咱们一起来理解下吧。
看这段代码 9 ~ 11 行,静态内部类 SingletonHelper 中的静态属性 instance 是随着 SingtonHelper 的加载而加载的,也就是说 一旦静态内部类 SingletonHelper 加载,就会对 Singleton 类进行实例化 ;
那问题来了,静态内部类 SingletonHelper 什么时候加载呢?
要注意的是,静态内部类不同于静态属性,不会随着宿主类(Singleton)的加载而加载,是在第一次调用静态内部类的时候再由 Java 虚拟机进行加载(代码第 6 行),这样就避免了多线程的同步问题。
同时,这种写法也实现了懒加载,如果不调用 newInstance()
方法,就不会调用静态内部类对 Singleton 进行实例化。
以上,我们一起分析了 5 种单例模式的写法,实际上他们都存在一个相同的问题:可序列化的单例类的反序列化问题。
存在此问题的前提是,单例类实现了 Serializable
接口,是可序列化的。
这样,我们通过序列化可将单例类的实例对象写到磁盘,然后再读回来,及进行反序列化,就可以得到此单例类的一个实例。即使构造函数是私有的,也是有办法获得单例类的一个新的实例的。
那怎么解决此问题呢?两种方案:
方案 1、利用类中可用的一个私有的钩子函数 readResolve()
,用来控制类的反序列化。看下代码示例吧。
import java.io.ObjectStreamException;
import java.io.Serializable;
public class Singleton implements Serializable{
private static final long serialVersionUID = 0L;
private static volatile Singleton instance;
// 私有构造,不允许外部通过构造实例化 Singleton.class
private Singleton() {
}
public static Singleton newInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 钩子函数,用来控制类的反序列化操作
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
看这段代码 26 ~ 28 行,通过此钩子函数,当对类进行反序列化时,返回我们的单例的实例 instance
,就避免了反序列化时创建新实例的问题。
方案 2:就是下面咱们要说的第 6 种实现单例模式的写法:枚举模式。
我们知道,枚举在 Java 中与普通的类是一样的,可以有自己的属性,还可以有自己的方法,最重要的是,默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例(任何情况包括反序列化)。
让我们来看下代码示例吧。
public enum Singleton {
INSTANCE;
}
哈哈哈,有没有被惊掉下巴?没错,枚举实现单例的代码就是这么简单!
那怎么获得枚举实现的单例类实例呢?也是十分简单。
Singleton instance = Singleton.INSTANCE;
如果哪位小伙伴对枚举不熟悉的,文后有一个枚举详解的文章链接,很简单,学一下吧。
这算是一种比较另类的写法了。还是先来看代码示例,至于这种写法的优劣,请各位自判吧。
import java.util.HashMap;
import java.util.Map;
public class SingletonManager {
// 存储单例类的容器
private static Map mSingletonMap = new HashMap<>();
// 不需要对此 单例管理类 进行实例化
private SingletonManager() {
}
// 向容器中注册单例类
public static void registerSingleton(String key, Object instance) {
if (!mSingletonMap.containsKey(key)) {
mSingletonMap.put(key, instance);
}
}
// 从容器中获取单例类
public static Object getSingleton(String key) {
return mSingletonMap.get(key);
}
}
在程序初始时,会将多种单例类型的对象注入到容器中,当需要使用时通过 key 从容器中取出相应的单例类型的对象。
这种写法,很明显可以很好对单例类型进行统一管理,存取操作对用户是透明且低耦合的,问题是容器中初始的单例类型会消耗资源,无论是否会被用到。
在 Android 系统中,系统级别的服务用的就是这种方案,如 AMS、WMS、LayoutInflater 等服务,会在合适的时候以单例的形式注册到系统中,当需要调用相应服务的时候,会通过 Context 的 getSystemService(String name) 获取。
至此,本文要介绍的 7 种单例模式的写法就介绍完了。
无论是哪种实现方式,核心原理都是首先将构造函数私有化,然后通过静态方法得到相应类的实例。
至于选用哪一种,大家要根据相应单例类的应用场景决定,比如是否是高并发环境、JDK 版本是否 <= 1.5、相应类的实例对资源的消耗情况、对相应类实例的取用频率等。
单例的模式的应用范围很广,具体的使用场景有以下几个:
1、整个项目需要一个共享访问点或者需要共享数据;
2、创建相应对象需要耗费大量资源,如 I/O 操作、数据库连接等;
3、工具类对象;
等。
单例模式写到这里,真心体会到认为简单的东西不见得真的简单,做技术是一定要沉下心的,不仅知道怎么用,还要知道为什么。
另外,无论是看技术博客,还是看技术书籍,一定要多方比对着看,互相印证,理解更深,且可以发现有些博文,甚至有些书籍常会存在解释不准确的情况。