Java设计模式学习笔记(五) 单例模式
前言
本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址
1. 使用单例模式的原因
以Windows任务管理器为例,在Windows系统中,任务管理器是唯一的,多次打开任务管理器,始终只能弹出一个唯一的任务管理器.
这么做的理由有两个:
- 节约资源
- 避免多个实例数据不一致问题
1.1 节约资源
如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口
1.2 避免多个实例数据不一致问题
如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这纯属“调戏”用户,给用户带来误解,更不可取.
为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在
2. 单例模式概述
通过模拟Windows任务管理器,创建TaskManager类来理解单例模式
2.1 TaskManager
/**
* @author liuboren
* @Title: 任务管理器类
* @Description:
* @date 2019/7/16 15:17
*/
public class TaskManager {
//初始化窗口
public TaskManager() {
}
//显示进程
public void displayProcesses() {
}
//显示服务
public void displayServices() {
}
}
2.2 重构TaskManager
为了实现Windows任务管理器的唯一性,我们通过如下三步来对该类进行重构:
- 构造函数使用private修饰
- 定义一个TaskManager类型的私有成员变量
- 增加共有静态方法实例化TaskManager
2.2.1 构造函数使用private修饰
由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因此需要将TaskManager的构造函数的可见性改为private,如下代码所示:
private TaskManager() {……}
2.2.2 定义一个TaskManager类型的私有成员变量
将构造函数改为private修饰后该如何创建对象呢?不要着急,虽然类的外部无法再使用new来创建对象,但是在TaskManager的内部还是可以创建的,可见性只对类外有效。因此,我们可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,如下代码所示:
private static TaskManager tm = null;
2.2.3 增加共有静态方法实例化TaskManager
为了保证成员变量的封装性,我们将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,
如下代码所示:
public static TaskManager getInstance()
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}
2.2.4 代码
在类外我们无法直接创建新的TaskManager对象,但可以通过代码TaskManager.getInstance()来访问实例对象,第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例,从而确保实例对象的唯一性
/**
* @author liuboren
* @Title: 单例版任务管理器类
* @Description:
* @date 2019/7/16 15:24
*/
public class TaskManagerSingleton {
private static TaskManagerSingleton taskManagerSingleton = null;
//初始化窗口
private TaskManagerSingleton() {
}
public static TaskManagerSingleton getInstance(){
if (taskManagerSingleton == null){
taskManagerSingleton = new TaskManagerSingleton();
}
return taskManagerSingleton;
}
//显示进程
public void displayProcesses() {
}
//显示服务
public void displayServices() {
}
}
2.3 定义
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式是一种对象创建型模式。
2.4 三要点
1.某个类只能有一个实例.
2.它必须自行创建这个实例.
3.是它必须自行向整个系统提供这个实例.
2.5 结构图
2.6 角色
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
3. 饿汉式单例类和懒汉式单例类
public static TaskManagerSingleton getInstance(){
if (taskManagerSingleton == null){
taskManagerSingleton = new TaskManagerSingleton();
}
return taskManagerSingleton;
}
在高并发环境下,以上代码是非线程安全的,可能在同一时刻,有两个线程通过 if (taskManagerSingleton == null)的判断,导致会实例化两次对象.
要想解决这个问题,有两种方式一种是懒汉式单例类.另一种是饿汉式单例类.
注: 如果对并发没有过了解,可以看我之前的博客并发编程学习笔记系列
3.1 饿汉式单例类
/**
* @author liuboren
* @Title: 饿汉式单例
* @Description:
* @date 2019/7/16 16:30
*/
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。饿汉式单例类可确保单例对象的唯一性。
3.2 懒汉式单例类
/**
* @author liuboren
* @Title: 懒汉式单例类
* @Description:
* @date 2019/7/16 16:41
*/
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
3.2.1 饿汉式单例类性能优化
该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
3.2.2 使用双重检查锁实现饿汉式单例类
问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。
原因如下:假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
/**
* @author liuboren
* @Title: 完美版 懒加载单例类
* @Description:
* @date 2019/7/16 16:47
*/
public class LazySingletonPerfect {
private volatile static LazySingletonPerfect instance = null;
private LazySingletonPerfect() {
}
public static LazySingletonPerfect getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingletonPerfect(); //创建单例实例
}
}
}
return instance;
}
}
3.3 饿汉式单例类与懒汉式单例类比较
3.3.1 饿汉式
优点:
-
线程安全: 饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性
-
调用速度优于懒汉式: 从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例
缺点: 资源利用效率不如懒汉式且加载时间较长无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长
3.3.2 懒汉式
优点:
- 延迟加载: 懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载
缺点:
-
非线程安全: 多线程访问懒汉式单例类可能会创建多个实例
-
性能稍差通过双重检查锁定等机制保证线程安全,这将导致系统性能受到一定影响
4.更好的单例实现方法
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。
可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术
/**
* @author liuboren
* @Title: IoDh技术
* @Description: Java最好的单例实现模式
* @date 2019/7/17 10:25
*/
public class IoDHSingleton {
private IoDHSingleton() {
}
//静态内部类的成员变量才能是静态的
private static class HolderClas{
public static IoDHSingleton ioDHSingleton = new IoDHSingleton();
}
public static IoDHSingleton getInstance(){
return HolderClas.ioDHSingleton;
}
public static void main(String[] args) {
IoDHSingleton s1,s2;
s1 = IoDHSingleton.getInstance();
s2 = IoDHSingleton.getInstance();
System.out.println(s1 == s2);
}
}
编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)
5. 总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用
5.1 优点
(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
5.2 缺点
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
(3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失
5.3 适用场景
(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。