前言:
为什么之前写过Golang 版的设计模式,还在重新写Java 版?
答:因为对于我而言,当然也希望对正在学习的大伙有帮助。Java作为一门纯面向对象的语言,更适合用于学习设计模式。
为什么类图要附上uml
因为很多人学习有做笔记的习惯,如果单纯的只是放一张图片,那么学习者也只能复制一张图片,可复用性较低,附上uml,方便有新理解时,快速出新图。
饿汉单例
@startuml class Singleton { - static instance = new Singleton + static getInstance(): Singleton + operation(): void } Singleton --> Singleton : getInstance() @enduml
懒汉单例
@startuml class Singleton { - static instance = null - constructor: Singleton + static getInstance(): Singleton + operation(): void } Singleton --> Singleton : getInstance() @enduml
懒汉加锁
@startuml class Singleton { - static volatile instance = null - constructor: Singleton + static getInstance(): synchronized Singleton + operation(): void } note left of Singleton::getInstance if (instance == null) instance = construct(); return instance; end note Singleton --> Singleton : getInstance() @enduml
懒汉双重检测
@startuml class Singleton { - static volatile instance = null - constructor: Singleton + static getInstance(): synchronized Singleton + operation(): void } note left of Singleton::getInstance // 一重检测 if (instance == null){ synchronized(..) { // 二重检测 if(instance == null) { instance = construct(); } } } return instance; end note Singleton --> Singleton : getInstance() @enduml
代码实现
饿汉
public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); // 私有构造方法,防止外部实例化 private EagerSingleton() { } // 获取单例对象的静态方法 public static EagerSingleton getInstance() { return instance; } }
EagerSingleton
类的构造方法被声明为私有,以防止外部通过 new
关键字实例化该类。而类中定义了一个私有静态变量 instance
,它在类加载的时候就被创建并初始化为 EagerSingleton
类的一个实例。
getInstance()
方法是获取单例对象的静态方法,它直接返回了已经创建好的 instance
对象。
由于饿汉单例在类加载的时候就创建了对象,因此它具有线程安全的特性,但可能会造成一定的资源浪费,因为无论是否使用该单例对象,都会被提前创建。
懒汉单锁
public class LazySingleton { private static LazySingleton instance; // 私有构造方法,防止外部实例化 private LazySingleton() { } // 获取单例对象的静态方法,使用synchronized关键字实现线程安全 public static synchronized LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
懒汉双重检测
public class LazySingleton { private static volatile LazySingleton instance; // 私有构造方法,防止外部实例化 private LazySingleton() { } // 获取单例对象的静态方法,使用双重检测实现延迟加载和线程安全 public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
在上面的代码中,LazySingleton
类的构造方法被声明为私有,以防止外部通过 new
关键字实例化该类。类中定义了一个私有静态变量 instance
,它在第一次调用 getInstance()
方法时才会被创建并初始化为 LazySingleton
类的一个实例。
getInstance()
方法是获取单例对象的静态方法。在方法中,首先检查 instance
是否为空,如果为空则进入同步代码块。在同步代码块内部,再次检查 instance
是否为空,这是为了防止多个线程同时通过了第一个检查而进入同步块,
从而创建多个实例。如果 instance
仍然为空,则创建新的实例并将其赋值给 instance
。
使用 volatile
关键字修饰 instance
变量可以保证变量的可见性,从而避免在多线程环境下出现问题。
懒汉双重检测通过双重检查和同步块的方式实现了延迟加载和线程安全,同时也提高了性能。因此,它是一种常见的单例模式实现方式。
饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据内存;
懒汉式单例类线程安全控制烦琐,而且性能受影响。
可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题。
有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是肯定的。下面来学习这种更好的被称为 Initialization on Demand Holder(IoDH) 的技术。实现 IoDH 时,需在单例类中增加一个静态 (static) 内部类,在该内部类中创建单例对象,再将该单例对象通过 getInstance() 方法返回给外部使用
IODH
实现懒汉模式
public class LazySingleton { private static class SingletonHolder { private static final LazySingleton instance = new LazySingleton(); } // 私有构造方法,防止外部实例化 private LazySingleton() { } // 获取单例对象的静态方法 public static LazySingleton getInstance() { return SingletonHolder.instance; } }
在上面的代码中,LazySingleton
类内部定义了一个私有静态内部类 SingletonHolder
。在 SingletonHolder
内部,定义了一个私有静态变量 instance
,它是 LazySingleton
类的一个实例。
由于静态内部类 SingletonHolder
只有在 getInstance()
方法被调用时才会被加载,所以在类加载的过程中并不会创建 instance
对象。只有当第一次调用getInstance()
方法时,SingletonHolder
类会被加载,从而创建并初始化 instance
对象。这种方式利用了 Java
类加载的线程安全性和延迟加载的特性,实现了懒汉模式。
因此,通过 IODH
实现懒汉模式,可以在需要时延迟创建单例对象,并且保证了线程安全性。
单例模式总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
1.主要优点单例模式的主要优点如下:
(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。(3)允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称之为多例类。)
单例模式的主要缺点如下:
(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
(3)现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景在以下情况下可以考虑使用单例模式:
(1)系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2)客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。