一、什么是单例模式:
单例(Singleton)模式是一种常用的创建型设计模式。
简单来说就是一个类只能构建一个对象的设计模式。
核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
二、单例模式的应用场景:
1、需要生成唯一序列的环境
2、需要频繁实例化然后销毁的对象。
3、创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
4、方便资源相互通信的环境
举个例子:
1、windows桌面上的回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。
也就是说整个windows系统运行过程中只会维护一个回收站实例。
2、一般网站上统计实时在线人数的计数器也是单例模式。
三、单例模式的优缺点:
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:
1、不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3、单例类的职责过重,在一定程度上违背了“单一职责原则”。
四、单例模式的实现:
单例模式大致的实现步骤:
1、私有构造函数,防止被实例化
2、持有私有静态实例
3、公开静态工厂方法,获取唯一可用的对象
单例模式的几种实现方式:
1、饿汉式
它是在类装载时实例化对象,所以不支持懒加载,但它是线程安全的,也是平常使用较多的一种方式。
/** 单例模式-饿汉式-线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){} // 单例对象 类加载时创建instance 避免了多线程同步问题 private static Singleton instance = new Singleton(); // 静态工厂方法,获取唯一可用的对象 public static Singleton getInstance() { return instance; } }
2、懒汉式
需要用时实例化对象,支持懒加载,非线程安全。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
/** 单例模式-懒汉式-非线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){} // 单例对象 此处赋值为null,目的是实现延迟加载 private static Singleton instance = null; // 静态工厂方法,创建唯一可用的对象 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
那么如何让他支持多线程,变成线程安全呢? 加锁 synchronized
/** 单例模式-懒汉模式-线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){}; // 单例对象 此处赋值为null,目的是实现延迟加载 private static Singleton instance = null; // 静态工厂方法,创建唯一可用的对象 public static synchronized Singleton getInstance(){ if (instance == null) { instance = new Singleton(); } return instance; } }
这种方式具备很好的懒加载(lazy loading),能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
那么继续优化看第三种实现方式 ↓
3、双检锁/双重校验锁
这种方式采用双锁机制支持懒加载、线程安全且在多线程情况下能保持高性能。
/** 单例模式-双检锁/双重校验锁-线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){}; // 单例对象 此处赋值为null,目的是实现延迟加载 private static Singleton instance = null; // 静态工厂方法,创建唯一可用的对象 public static Singleton getInstance(){ // 双检锁/双重校验锁 if (instance == null) { //同步锁 synchronized(Singleton.class){ if (instance == null) { instance = new Singleton(); } } } return instance; } }
认真看的同学可能会发现其实这个Singleton 类虽然也加了锁synchronized 但是并没有解决多线程问题。
试想线程A走到第11行代码,Singleton 类第一次创建实例,同时线程B进来走到第8行代码。
这种情况下线程B第8行代码中 if (instance == null)就很有可能返回false,从而获取到未初始化完成的 instance。
为什么 if (instance == null) 会有可能返回false 呢? 这里就涉及到了JVM编译器和CPU的指令重排。
指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。
/** 单例模式-双检锁/双重校验锁-线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){}; // 单例对象 此处赋值为null,目的是实现延迟加载 // 添加 volatile 是为了操作此对象时防止JVM和CPU指令重排 private volatile static Singleton instance = null; // 静态工厂方法,创建唯一可用的对象 public static Singleton getInstance(){ // 双检锁/双重校验锁 if (instance == null) { //同步锁 synchronized(Singleton.class){ if (instance == null) { instance = new Singleton(); } } } return instance; } }
volatile修饰符阻止了变量访问前后顺序的指令重排,保证了指令的执行顺序。
如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
5、登记式/静态内部类
这种方式通过静态内部类支持懒加载、线程安全且在多线程情况下能保持高性能。
/** 单例模式-登记式/静态内部类-线程安全 */ public class Singleton { // 私有构造函数,防止被实例化 private Singleton(){}; // 此处使用一个内部类来维护单例 private static class SingletonFatory{ private static Singleton instance = new Singleton(); } // 静态工厂方法,获取唯一实例对象 public static Singleton getInstance(){ return SingletonFatory.instance; } // 如果该对象被用于序列化,可以保证对象在序列化前后保持唯一性 public Object readResolve() { return getInstance(); } }
instance 对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类SingletonFatory 被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。代码中readResolve()方法在下面补充。
6、枚举这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化(防止反射构造对象,反射构造对象会在下面做具体补充),不过不支持懒加载。
package test; /** 单例模式-枚举-线程安全 */ public enum Singleton { INSTANCE; public void whateverMethod(){ System.out.println("枚举类型实现单例模式!"); } public static void main(String[] args) { Singleton.INSTANCE.whateverMethod(); } }
为了好理解点我加了个main方法实现调用,这样一看可能还会有点蒙(大神跳过),下面我写一种好理解点的。
package test; /** 单例模式-枚举-线程安全 */ public class EnumSingleton { // 私有构造函数 private EnumSingleton(){}; public static EnumSingleton getInstance(){ return Singleton.INSTANCE.getInstance(); } //枚举-静态常量,隐式地用static final修饰过 private enum Singleton{ INSTANCE; private EnumSingleton singleton; //JVM会保证此方法绝对只调用一次 //枚举实际上是类,这里是构造方法 private Singleton(){ singleton = new EnumSingleton(); } public EnumSingleton getInstance(){ return singleton; } } }
下面做两点补充:
1、如果该单例类需要序列化则需加 readResolve() 方法,来确保对象在序列化前后保持唯一性;
具体实现在上面第5种实现方式代码里有增加readResolve() 方法。
2、反射构造对象:
以上第1-5种单例实现方式都有一个共同的问题:无法防止利用反射构造对象重复构建对象,下面我们在饿汉式单例模式的基础上来实现一下反射构造对象。
代码可以简单归纳为三个步骤:
1、获得单例类的构造器。
2、把构造器设置为可访问。
3、使用newInstance方法构造对象。
/** 单例模式-饿汉式-线程安全 */ public class Singleton3 { // 私有构造函数,防止被实例化 private Singleton3(){} // 单例对象 类加载时创建instance 避免了多线程同步问题,但容易产生垃圾对象 private static Singleton3 instance = new Singleton3(); // 静态工厂方法,获取唯一可用的对象 public static Singleton3 getInstance() { return instance; } public static void main(String[] args) throws Exception{ //获得构造器 Constructor con = Singleton.class.getDeclaredConstructor(); //设置为可访问 con.setAccessible(true); //构造两个不同的对象 Singleton singleton1 = (Singleton)con.newInstance(); Singleton singleton2 = (Singleton)con.newInstance(); //验证是否是不同对象 System.out.println(singleton1.equals(singleton2)); } }
运行结果:
false
最后为了确认这两个对象是否真的是不同的对象,我们使用equals方法进行比较。毫无疑问,比较结果是false。
第6种实现单例模式的方法(枚举)可以有效防止反射构造对象。