单例设计模式
一、概述
1、什么是单例设计模式?
在某些特殊场合中,一个类只能够产生一个实例对象,并且这个实例对象要可以对外提供访问。这样的类叫做单例类, 而设计单例的流程和思想叫做单例设计模式。
单例模式属于设计模式三大类中的创建型模式。
2、单例设计模式的特点
单例模式具有典型的三个特点:
- 只有一个实例。
- 自我实例化。
- 提供全局访问点。
注意:
注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。 (这句话表述的有点问题,单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。单例类与继承没有强关联关系。)
3、单例设计模式的UML类图
单例模式的UML结构图非常简单,就只有一个类,如下图:
Singleton类,定义一个静态方法,getInstance(),可以通过类名来调用,主要负责替代构造方法,创建Singleton类唯一的实例对象。
这个类可以对外提供访问,允许用户通过getInstance()方法访问它唯一的实例。
4、单例设计模式的优缺点:
优点:
1)、由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,
2)、避免频繁的创建销毁对象,可以提高性能;
3)、避免对共享资源的多重占用,简化访问;
4)、为整个系统提供唯一一个全局访问点,能够严格控制客户对它的访问。
缺点:
1)、不适用于变化频繁的对象;
2)、也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,
滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
3)、同时也没有抽象类,这样扩展起来有一定的困难。
4)、如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;(这个所有的对象都会,跟单例无关。)
5、单例模式的应用场景:
场景一:
windows的任务管理器,无论你点击多少次,始终都只有一个管理器窗口存在,系统并不会为你创建新的窗口,也就是说,整个系统运行的过程中,系统只维护了一个进程管理器的实例。这就是一个典型的单例模式运用。
场景二:
线程池、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,用单例模式来维护,就可以大大降低这种损耗。
场景三:
程序的日志模块。一般也是采用单例模式实现。由于共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。 采用单例模式就可以。
场景四:
Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
这些配置信息存放在一个文件中,由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
场景五:
在我们的实际项目开发中,可以使用单例模式来封装一些常用的工具类,保证整个应用常用的数据统一。或者保存一些共享数据在内存中,其他类随时可以读取。
二、单例模式的实现步骤
可以使用如下的步骤实现一个单例类:
单例设计模式的实现流程
1、将构造方法私有化,使用private关键字修饰。使其不能在类的外部通过new关键字实例化该类对象。 2、在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。 3、对外提供一个静态方法getInstance()负责将对象返回出去,使用public static修饰
三、单例模式的实现方式 (推荐枚举类方式)
1、饿汉式——立即加载
线程安全,调用效率高。但是不能延时加载。
立即加载就是加载类的时候就已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故又被称为“饿汉模式”),常见的实现办法就是直接new实例化。
所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。
/** * 饿汉式 */ public class Singleton { // 创建全局唯一的实例化对象,在类初始化时,就会立即加载这个对象 private static Singleton instance = new Singleton(); // 私有化构造方法 private Singleton() {} // 提供公有静态方法返回对象 public static Singleton getInstance() { return instance; } }
我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
优缺点:
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。(因为这个static的instance对象会一直占着这段内存,直到卸载类(即便你还没有用到这个实例))
2、懒汉式——延迟加载
延迟加载就是调用get()方法时实例才被创建(先不急着实例化出对象,等要用的时候才给你创建出来。不着急,故又称为“懒汉模式”),常见的实现方法就是在get方法中进行new实例化。
/** * 懒汉式 */ public class Singleton { // 声明一个自身实例对象的引用 private static Singleton instance; // 私有化构造方法 private Singleton(){} // 提供公有静态方法返回对象 public static Singleton getInstance() { // 判断如果为空,就创建,如果已经有了,就直接返回该实例,避免重复创建,保证全局唯一 if (instance == null) { instance = new Singleton(); } return instance; } }
由于该模式是在运行时加载对象的,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全。如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。
我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
“懒汉模式”的优缺点:
优点:实现起来比较简单,当类SingletonTest被加载的时候,静态变量static的instance未被创建,只是声明了一个引用,并未分配内存空间。要当getInstance方法第一次被调用时,初始化instance变量,才会真正创建对象,开始分配内存,因此在某些特定条件下会节约了内存。(需要时才创建)
缺点:在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态。
3、线程安全的“懒汉模式”—— synchronized
在懒汉模式的基础上,增加了synchronized锁同步机制,保证全局唯一。
/** * 3、线程安全的懒汉式 —— synchronized */ public class Singleton { // 声明一个自身实例对象的引用 private static Singleton instance; // 私有化构造方法 private Singleton(){} // 提供公有静态方法返回对象,加上synchronized关键字实现同步 public static synchronized Singleton getInstance() { // 判断如果为空,就创建,如果已经有了,就直接返回该实例,避免重复创建,保证全局唯一 if (instance == null) { instance = new Singleton(); } return instance; } }
优点:在多线程情形下,保证了“懒汉模式”的线程安全。
缺点:众所周知在多线程情形下,synchronized方法通常效率低,显然这不是最佳的实现方案。
4、懒汉式(DCL双重检测锁)
DCL双检查锁机制(DCL:double checked locking)
/** * 4、懒汉式 —— DCL双重检查锁机制(类锁) * 再一次缩小了锁的范围,提供了性能 */ public class Singleton { // 声明一个自身实例对象的引用,使用volatile保证多线程下引用的一致性 private static volatile Singleton instance; // 私有化构造方法 private Singleton(){} // 提供公有静态方法返回对象 public static Singleton getInstance() { // 第一次检查instance是否被实例化出来,如果没有,再加锁处理 if (instance == null) { synchronized (Singleton.class) { // 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象 if (instance == null) { instance = new Singleton(); } } } return instance; } }
Double-Check概念对于多线程开发者来说不会陌生。
我们这里相比3直接对静态方法getInstance加上synchronized锁的方式,缩小了锁的范围。
将第一个if判断块释放出来了,如果实例存在,则根本不会锁住,大大加快了返回实例的效率。
只有当第一次if检查后,确定实例是真的不存在,需要创建时,此时才会开始加锁,注意此时加的是类锁,不是对象锁。不过这里是static静态方法,对象也是静态的,所以实际上它们效果是一样的。
为什么在锁里面还要再次判定是否为空呢?
因为高并发,后面的线程在第一次判定实例时也为空,也可以获得锁,只是要排队,只是在等待前面的线程释放锁。所以,当轮到它拿到锁之后,可能前面的线程已经创建了实例,所以要再次判定是否为空。这样才能保证实例唯一。
(重点在于,多个线程可以同时通过第一个if,然后都可以按顺序执行锁里的代码。)
Java指令重排的问题
注意:单纯使用上面这种方式,仍然是线程不安全的。
因为存在java指令重排的问题。
在java创建对象的时候,cpu按照以下三个步骤来执行:
1、memory = allocate() 在堆内存中开辟对象的内存空间,并指定地址
2、根据类加载的顺序,初始化对象。
3、instance = memory 设置instance指向刚分配的内存地址。instance是变量,存在栈中。
单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。
指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。
如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:
1、memory = allocate() 在堆内存中开辟对象的内存空间,并指定地址
3、instance = memory 设置instance指向刚分配的内存地址。instance是变量,存在栈中。
2、根据类加载的顺序,初始化对象。
假设目前有两个线程A和B同时执行getInstance()方法,
- A线程执行到instance = new Singleton(); B线程刚执行到第一个 if (instance == null) 处,
- 如果按照1.3.2的顺序,假设线程A执行到第三步3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;
- 而实际上,线程A还未执行第二步 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。
5、懒汉式(DCL双重检测锁机制+volatile禁止指令重排)—— 推荐
相比4,这里对引用加入了volatile机制,禁止java的指令重排
懒汉式的单例模式的最佳实现方式。内存消耗少,效率高,线程安全,多线程操作原子性。
/** * 5、懒汉式 —— DCL双重检查锁机制(类锁) + volatile禁止指令重排 * 再一次缩小了锁的范围,提供了性能。(推荐) */ public class Singleton { // 声明一个自身实例对象的引用,使用volatile禁止指令重排,保证多线程下引用的一致性 private static volatile Singleton instance; // 私有化构造方法 private Singleton(){} // 提供公有静态方法返回对象 public static Singleton getInstance() { // 第一次检查instance是否被实例化出来,如果没有,再加锁处理 if (instance == null) { synchronized (Singleton.class) { // 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象 if (instance == null) { instance = new Singleton(); } } } return instance; } }
线程安全;延迟加载;效率较高。
6、静态代码块——立即加载
静态代码块方式跟饿汉式的方式几乎是一样的,只是把初始化代码放到了static块中了。
因为我们知道,类加载的时候,这些属性和静态代码块都是会跟随类一起加载的,所以它的实现方式和饿汉式一样。也是线程安全的。
/** * 6、静态代码块方式 * 方式类似饿汉式,也是立即加载,是线程安全的 */ public class Singleton { // 在外部声明一个对象的引用,注意不能放到静态代码块中 private static Singleton instance; // 静态代码块中,创建唯一实例对象,赋值给引用。 static { instance = new Singleton(); } // 私有化构造方法 private Singleton() {} // 提供公有静态方法返回实例对象 public static Singleton getInstance() { return instance; } }
优缺点:
优缺点都同饿汉式一样,也是立即加载,线程安全的。
这里定义静态变量时要注意:
静态变量只能定义在类的内部,不可以定义在静态块或方法中。可以在类内部定义静态变量,在静态块中进行初始化操作,因为类的内部是不允许有操作语句存在的,比如JDBC操作,所以可以在静态块static{} 中进行初始化操作,如:JDBC 定义静态变量主要是为了供外部访问,定义在一个局部中外部没有权限访问,为什么要定义呢,而且不能定义。
7、静态内部类
懒汉模式需要考虑线程安全,所以我们多写了好多的代码,饿汉模式利用了类加载的特性为我们省去了线程安全的考虑,那么,既能享受类加载确保线程安全带来的便利,又能延迟加载的方式,就是静态内部类。Java静态内部类的特性是,加载的时候不会加载内部静态类,使用的时候才会进行加载。而使用到的时候类加载又是线程安全的,这就完美的达到了我们的预期效果~
/** * 7、静态内部类 * 融合饿汉式和懒汉式的优点,推荐 */ public class Singleton { // 私有静态内部类中创建并初始化实例对象,注意要private私有化,不能被外部调用了 private static class SingletonInner{ private static Singleton instance = new Singleton(); } // 私有化构造方法 private Singleton() {} // 提供公有静态方法,返回实例对象 public static Singleton getInstance() { return SingletonInner.instance; } }
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。
8、枚举类 —— 线程最安全(最佳方式)
单元素的枚举类型已经成为实现Singleton的最佳方法
-- 出自 《effective java》
在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
/** * 8、枚举类 * 最佳实现方式 */ public enum Singleton { INSTANCE; }注意:
因为INSTANCE实例是public公有的,可以直接通过类名的方式调用,即Singleton.INSTANCE,
就不再需要提供公有静态方法getInstance()来返回对象了。
这是最简洁、最安全的方式,不过它不能实现lazy loading延迟加载。
其实枚举类它本身就具备单例的特性:
比如:都会私有化构造方法。枚举类会对属性值加上public static final的属性,保障这个属性值都是全局唯一的。这些操作都和单例很像
所以把这个属性变成对象,它就是一个单例类。
类似于这种内部类的形式:
public class Singleton { public static final Singleton INSTANCE = new Singleton(); }
枚举类继承自ENUM,内部实现了Serializable接口,所以不用考虑序列化的问题(其实序列化反序列化也能导致单例失败的,但是我们这里不过多研究)。
对于线程安全,同样的,加载的时候JVM能确保只加载一个实例。避免暴力反射创建多个实例,绝对防止多次实例化。
枚举类最佳实践:
参考:https://www.jianshu.com/p/d35f244f3770
枚举单例示例:
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
实际应用场景中,很多人会这么使用枚举单例:
public class User { //私有化构造函数 private User(){ } //定义一个静态枚举类 static enum SingletonEnum{ //创建一个枚举对象,该对象天生为单例 INSTANCE; private User user; //私有化枚举的构造函数 private SingletonEnum(){ user=new User(); } public User getInstnce(){ return user; } } //对外暴露一个获取User对象的静态方法 public static User getInstance(){ return SingletonEnum.INSTANCE.getInstnce(); } } public class Test { public static void main(String [] args){ System.out.println(User.getInstance()); System.out.println(User.getInstance()); System.out.println(User.getInstance()==User.getInstance()); } } 结果为true
以上代码看起来已经是ok了,其实不是,可能还存在反射攻击或者反序列化攻击
最终版
public enum Singleton { INSTANCE; public void doSomething() { System.out.println("doSomething"); } } // 调用方法: public class Main { public static void main(String[] args) { Singleton.INSTANCE.doSomething(); } } // 直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。
推荐大家使用枚举类实现单例模式。
四、各种实现方式的选择
一般情况下,懒汉式(包含线程安全和线程不安全两种方式)都比较少用;
饿汉式和DCL双重检测锁都可以使用,可根据具体情况自主选择;
在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;
若涉及到反序列化创建对象时,大家也可以尝试使用枚举方式。
在选择时,请参考下面这张图:
图片来源:https://www.cnblogs.com/rainbowbridge/p/12902359.html
五、破坏单例模式的方法及解决办法
参考:https://blog.csdn.net/b_just/article/details/104061314
1、除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:
private SingletonObject1(){ if (instance !=null){ throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取"); } }
2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
public Object readResolve() throws ObjectStreamException { return instance; }
引用转载:
https://www.jianshu.com/p/3f5eb3e0b050 (爆赞)
https://www.cnblogs.com/xuwendong/p/9633985.html (爆赞)
https://segmentfault.com/a/1190000010755849 (赞)
https://www.cnblogs.com/binaway/p/8889184.html