Gof23-Singleton模式
1. 模式动机
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。
一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
为了达到这个目的,必须设置构造器私有。
2. 模式定义
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:
- 一是某个类只能有一个实例;
- 二是它必须自行创建这个实例;
- 三是它必须自行向整个系统提供这个实例。
单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
3. 模式结构
单例模式只有角色:
- Singleton:单例
4. 模式分析
单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式包含的角色只有一个,就是单例类——Singleton。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
在单例模式的实现过程中,需要注意如下三点:
- 单例类的构造函数为私有;
- 提供一个自身的静态私有成员变量;
- 提供一个公有的静态工厂方法。
5. 分类
5.1. 饿汉单例模式
饿汉单例模式是一种常见的单例模式,它在类加载时就创建了单例对象,因此也被称为“饿汉模式”。这种模式的优点是实现简单,线程安全,但缺点是可能会浪费一些内存空间,因为即使不需要使用该单例对象,它也会被创建。
public class Hungry {
// 当这个对象拥有一个占用内存特别大的对象,那么会浪费很大一块内存空间
private byte[] buffer = new byte[1024*100];
static {
System.out.println("饿汉单例初始化,即将创建单例对象");
}
// 在类加载的初始化阶段时,就创建实例
private final static Hungry instance = new Hungry();
// 设置构造器私有
private Hungry() {
System.out.println("饿汉单例执行构造方法");
}
public static Hungry getInstance() {
return instance;
}
public static void main(String[] args) {
Hungry instance = Hungry.getInstance();
System.out.println(instance);
}
}
/**
* 运行结果:
* 饿汉单例初始化,即将创建单例对象
* 饿汉单例执行构造方法
* basic.designpattern.singleton.Hungry@1b6d3586
*/
5.2. 懒汉单例模式
懒汉单例模式在第一次调用 getInstance 方法时,实例化 LazySingleton 对象,在类加载时并不自行实例化,这种技术称为延迟加载技术(Lazy Load) ,即在需要的时候进行加载实例。
这种模式的优点是节省内存空间,缺点是可能有线程安全的问题
单线程下,代码安全:
public class LazySingleton {
private static LazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
// 构造器私有
private LazySingleton() {
System.out.println("LazySingleton 执行构造方法");
}
// 使用时才实例化对象
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
LazySingleton instance = LazySingleton.getInstance();
LazySingleton instance2 = LazySingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
/**
* 执行结果:
* LazySingleton 初始化...
* LazySingleton 执行构造方法
* basic.designpattern.singleton.LazySingleton@1b6d3586
* basic.designpattern.singleton.LazySingleton@1b6d3586
*/
可以看到只调用了一次构造方法,并且两个对象是相同的。
多线程环境下调用:
public class LazySingleton {
private static LazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
// 构造器私有
private LazySingleton() {
System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazySingleton.getInstance();
}).start();
}
}
}
/**
* 执行结果:
* LazySingleton 初始化...
* Thread-0LazySingleton 执行构造方法
* Thread-2LazySingleton 执行构造方法
* Thread-1LazySingleton 执行构造方法
*/
可以看到,当在多线程环境下,可能多个线程同时调用getInstance()方法,并且同时调用了构造方法,导致生成多个实例对象。这就不满足单例了。
所以代码需要优化。
5.3. DCL双重检测锁懒汉模式
因为懒汉模式在多线程环境下不安全,无法满足单例,所以需要对代码进行优化加锁处理。
DCL懒汉模式是指在懒汉模式的基础上,加入了双重检测机制,保证了线程安全,同时也保证了效率。
DCL懒汉模式的实现方式是:在 getInstance() 方法中进行双重检查,第一次检查 instance 是否为 null,如果为 null,则进入同步代码块,再次检查 instance 是否为 null,如果还是 null,则创建实例。
DCL懒汉模式相对于懒汉模式来说,在多线程环境下更加安全,同时也不会影响程序的性能。
public class DCLLazySingleton {
private static DCLLazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
// 构造器私有
private DCLLazySingleton() {
System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
}
public static DCLLazySingleton getInstance() {
// 第一重检测,如果instance为空,则进行加锁
if (instance == null) {
synchronized (DCLLazySingleton.class) {
// 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
if (instance == null) {
instance = new DCLLazySingleton(); // 不是原子化操作
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
DCLLazySingleton.getInstance();
}).start();
}
}
}
/**
* 执行结果:
* LazySingleton 初始化...
* Thread-0LazySingleton 执行构造方法
*/
这个代码在极端环境下依旧可能出现错误,在JDK1.5之前,由于Java内存模型中存在缺陷,可能会导致DCL懒汉模式失效。 这个缺陷是由于Java内存模型中存在的指令重排问题导致的。
在JDK1.5之后,Java内存模型得到了改进,DCL懒汉模式已经可以正常工作了。
为了解决这个问题,可以使用volatile关键字来修饰instance变量,保证其可见性和有序性。
什么是指令重排?
指令重排是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段。 重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。
Java中,使用new关键字创建对象并不是一个原子性操作,它会经历这三个指令
- 分配内存空间
- 执行构造方法,初始化对象
- 把对象指向这块空间
如果CPU对这三个指令进行重拍,可能会有这样的顺序 1 -> 3 -> 2
回到刚刚的代码:
当线程A执行到同步代码块,执行new关键字实例化对象,此时CPU对new进行了指令重拍,1 -> 3 -> 2
线程A执行到3这个指令,instance变量已经指向一块空间,但对象还未初始化,此时线程B发现instance变量不为空,getInstacne()方法就直接返回了一个空对象。这就出现了线程安全问题。
为了解决这个问题,可以使用volatile关键字来修饰instance变量,保证其可见性和有序性。
volatile关键字:
在Java中,volatile关键字是一种轻量级的同步机制,它可以保证可见性和有序性。当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后,它会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
volatile关键字可以禁止指令重排,这是因为volatile关键字包含“禁止指令重排序”的语义。
public class DCLLazySingleton {
private volatile static DCLLazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
// 构造器私有
private DCLLazySingleton() {
System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
}
public static DCLLazySingleton getInstance() {
// 第一重检测,如果instance为空,则进行加锁
if (instance == null) {
synchronized (DCLLazySingleton.class) {
// 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
if (instance == null) {
instance = new DCLLazySingleton(); // 不是原子化操作
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这块空间
*/
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
DCLLazySingleton.getInstance();
}).start();
}
}
}
/**
* 执行结果:
* LazySingleton 初始化...
* Thread-0LazySingleton 执行构造方法
*/
5.4. 静态内部类实现单例模式
静态内部类实现单例模式是一种常见的单例模式实现方式。在Java中,静态内部类不会在外部类加载时被加载,只有在第一次使用时才会被加载,这种方式可以保证线程安全性和延迟加载。
public class HolderSingleton {
static {
System.out.println("HolderSingleton类加载中,初始化....");
}
public HolderSingleton() {
System.out.println("HolderSingleton构造方法执行");
}
public static Inner getInstance() {
return Inner.instance;
}
/**
* 静态内部类不会在外部类加载时被加载,只有在第一次使用时才会被加载
*/
private static class Inner {
static {
System.out.println("Inner类加载中,初始化....");
}
private static Inner instance = new Inner();
// 私有构造方法
private Inner() {
System.out.println("调用静态内部类构造方法");
}
}
public static void main(String[] args) {
Inner instance = HolderSingleton.getInstance();
Inner instance1 = HolderSingleton.getInstance();
System.out.println(instance1);
System.out.println(instance);
}
}
/**
* HolderSingleton类加载中,初始化....
* Inner类加载中,初始化....
* 调用静态内部类构造方法
* basic.designpattern.singleton.HolderSingleton$Inner@1b6d3586
* basic.designpattern.singleton.HolderSingleton$Inner@1b6d3586
*/
5.4. 使用反射破坏单例
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();
Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
// 无视构造器的private修饰
constructor.setAccessible(true);
DCLLazySingleton lazy2 = constructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
/**
* LazySingleton 初始化...
* mainLazySingleton 执行构造方法
* mainLazySingleton 执行构造方法
* basic.designpattern.singleton.DCLLazySingleton@1b6d3586
* basic.designpattern.singleton.DCLLazySingleton@4554617c
*/
如何应对:
在构造方法中加对象锁,
public class DCLLazySingleton {
// 使用volatile关键字保证可见性和有序性
private volatile static DCLLazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
// 构造器私有
private DCLLazySingleton() {
synchronized (DCLLazySingleton.class) {
if (instance != null) {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
}
public static DCLLazySingleton getInstance() {
// 第一重检测,如果instance为空,则进行加锁
if (instance == null) {
synchronized (DCLLazySingleton.class) {
// 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
if (instance == null) {
instance = new DCLLazySingleton(); // 不是原子化操作
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这块空间
*/
}
}
}
return instance;
}
}
再次破坏:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();
Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
// 无视构造器的private修饰
constructor.setAccessible(true);
// 使用反射生成的构造器破坏
DCLLazySingleton lazy2 = constructor.newInstance();
DCLLazySingleton lazy1 = constructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
/**
* LazySingleton 初始化...
* mainLazySingleton 执行构造方法
* mainLazySingleton 执行构造方法
* basic.designpattern.singleton.DCLLazySingleton@1b6d3586
* basic.designpattern.singleton.DCLLazySingleton@4554617c
*/
如何应对:
通过使用红绿灯的方法,添加一个隐藏变量
public class DCLLazySingleton {
// 使用volatile关键字保证可见性和有序性
private volatile static DCLLazySingleton instance;
static {
System.out.println("LazySingleton 初始化...");
}
private static boolean flag = false;
// 构造器私有
private DCLLazySingleton() {
synchronized (DCLLazySingleton.class) {
if (flag == false) {
flag = true;
} else {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
}
public static DCLLazySingleton getInstance() {
// 第一重检测,如果instance为空,则进行加锁
if (instance == null) {
synchronized (DCLLazySingleton.class) {
// 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
if (instance == null) {
instance = new DCLLazySingleton(); // 不是原子化操作
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这块空间
*/
}
}
}
return instance;
}
}
测试:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();
Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
// 无视构造器的private修饰
constructor.setAccessible(true);
// 使用反射生成的构造器破坏
DCLLazySingleton lazy2 = constructor.newInstance();
DCLLazySingleton lazy1 = constructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
/**
* LazySingleton 初始化...
* mainLazySingleton 执行构造方法
* Exception in thread "main" java.lang.reflect.InvocationTargetException
* at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
* at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
* at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
* at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
* at basic.designpattern.singleton.Test.main(Test.java:21)
* Caused by: java.lang.RuntimeException: 请不要使用反射破坏单例
* at basic.designpattern.singleton.DCLLazySingleton.<init>(DCLLazySingleton.java:25)
* ... 5 more
*/
再次破坏:
假设知道了隐藏变量的名称:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
// DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();
Field flag = DCLLazySingleton.class.getDeclaredField("flag");
// 破坏flag的private修饰
flag.setAccessible(true);
Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
// 无视构造器的private修饰
constructor.setAccessible(true);
// 使用反射生成的构造器破坏
DCLLazySingleton lazy2 = constructor.newInstance();
// 将值设置会false
flag.set(lazy2, false);
DCLLazySingleton lazy1 = constructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
/**
* LazySingleton 初始化...
* mainLazySingleton 执行构造方法
* mainLazySingleton 执行构造方法
* basic.designpattern.singleton.DCLLazySingleton@74a14482
* basic.designpattern.singleton.DCLLazySingleton@1540e19d
*/
5.5. 使用序列化破坏单例
public class TestSerializable {
public static void main(String[] args) {
DCLLazySingleton instance = DCLLazySingleton.getInstance();
System.out.println("====");
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("obj.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.txt"));
objectOutputStream.writeObject(instance);
Object o = objectInputStream.readObject();
System.out.println(instance);
System.out.println(o);
System.out.println(instance == o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* LazySingleton 初始化...
* mainLazySingleton 执行构造方法
* ====
* basic.designpattern.singleton.DCLLazySingleton@27973e9b
* basic.designpattern.singleton.DCLLazySingleton@312b1dae
* false
*/
5.6. 枚举实现单例模式
枚举是在JDK1.5以及以后版本中增加的一个“语法糖”,它主要用于维护一些实例对象固定的类。例如一年有四个季节,就可以将季节定义为一个枚举类型,然后在其中定义春、夏、秋、冬四个季节的枚举类型的实例对象。 按照Java语言的命名规范,通常,枚举的实例对象全部采用大写字母定义,这一点与Java里面的常量是相同的。
因为Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。
public enum EnumSingleton {
SINGLETON;
static {
System.out.println("EnumSingleton初始化");
}
private EnumSingleton() {
}
public static EnumSingleton getInstance() {
return SINGLETON;
}
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.getInstance();
System.out.println(instance);
}
}
/**
* EnumSingleton初始化
* SINGLETON
*/
枚举实现的单例模式不会受到反射和序列化的影响。
6. 优点
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
7. 缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
8. 适用环境
在以下情况下可以使用单例模式:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式
笔者学习设计模式的记录与心得。
