单例模式的分类饿汉式 懒汉式线程不安全 线程安全,同步方法 线程安全,同步代码块 双重检查锁 静态内部类 枚举 饿汉式饿汉式,单例模式的一种类型,对于这个名字可以假想成:
有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。
这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费
饿汉式-静态常量(⭐慎用)这是最简单的单例模式,主要有以下几点核心思路
私有构造方法 私有静态常量,类加载时初始化常量对象 公有对象获取方法 示例代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class SingletonType01 { public static void main (String[] args) { Singleton01 instance1 = Singleton01.getInstance(); Singleton01 instance2 = Singleton01.getInstance(); System.out.println("instance1 == instance2 " +(instance1==instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton01 { /** * 构造方法私有,防止外部实例化 */ private Singleton01 () { } /** * 在类加载时创建私有的静态变量 */ private final static Singleton01 INSTANCE = new Singleton01(); /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * @return 返回单例对象 */ public static Singleton01 getInstance () { return INSTANCE; } }
示例代码本机执行结果:
1 2 3 instance1 == instance2 true 491044090 491044090
主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同
优点:
写法简单,在类装载的时候完成实例化,避免线程同步问题 缺点:
在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果 这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题
饿汉式-静态代码块(⭐慎用)饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /** * * @author larsCheng */ public class SingletonType02 { public static void main (String[] args) { Singleton02 instance1 = Singleton02.getInstance(); Singleton02 instance2 = Singleton02.getInstance(); System.out.println("instance1 == instance2 : " +(instance1==instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton02 { /** * 构造方法私有,防止外部实例化 */ private Singleton02 () { } /** * 静态私有变量 */ private static Singleton02 INSTANCE; /** * 将对象的实例化放在了静态代码块中,同样也是类加载时被执行 */ static { INSTANCE = new Singleton02(); } /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * @return 返回单例对象 */ public static Singleton02 getInstance () { return INSTANCE; } }
示例代码本机执行结果:
1 2 3 instance1 == instance2 : true 491044090 491044090
可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费
懒汉式前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。
这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景
有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了
虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。
当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果
懒汉式-线程不安全(👎👎👎不可使用)懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class SingletonType03 { public static void main (String[] args) { Singleton03 instance1 = Singleton03.getInstance(); Singleton03 instance2 = Singleton03.getInstance(); System.out.println("instance1 == instance2 : " + (instance1 == instance2)); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Singleton03 { /** * 构造方法私有,防止外部实例化 */ private Singleton03 () { } /** * 静态私有变量 */ private static Singleton03 INSTANCE; /** * 对外提供获取对象的静态方法,此处存在线程安全问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton03 getInstance () { if (INSTANCE == null ) { INSTANCE = new Singleton03(); } return INSTANCE; } }
示例代码本机执行结果:
1 2 3 instance1 == instance2 : true 491044090 491044090
简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式
但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题
分析:
假设有两个线程A、B A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。 这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!
所以通常情况下,不推荐使用这种懒汉式的单例模式 。因为绝大多数的应用场景都为多线程环境。
而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例
懒汉式 - 同步方法(👎不推荐)针对于线程不安全问题,对应则有线程安全的解决方案
即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Singleton04 { /** * 构造方法私有,防止外部实例化 */ private Singleton04() { } /** * 静态私有变量 */ private static Singleton04 INSTANCE; /** * 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static synchronized Singleton04 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton04(); } return INSTANCE; } }
如上,虽然解决了线程不安全问题,但是随之而来的是效率问题
分析:
每次调用getInstance方法都需要进行线程同步 实际上造成多个对象被实例化的仅仅只是方法中代码片段 所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用
针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下
将同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Singleton04ErrorSolution { /** * 构造方法私有,防止外部实例化 */ private Singleton04ErrorSolution () { } /** * 静态私有变量 */ private static Singleton04ErrorSolution INSTANCE; /** * 对外提供获取对象的静态方法,对造成线程安全问题的代码块进行同步 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton04ErrorSolution getInstance () { if (INSTANCE == null ) { synchronized (Singleton04ErrorSolution.class) { INSTANCE = new Singleton04ErrorSolution(); } } return INSTANCE; } }
如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果
分析:
对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法 A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕 A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象 从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误 ,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!!
懒汉式-同步代码块(👎不推荐)基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Singleton05 { /** * 构造方法私有,防止外部实例化 */ private Singleton05 () { } /** * 静态私有变量 */ private static Singleton05 INSTANCE; /** * 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton05 getInstance () { synchronized (Singleton05.class) { if (INSTANCE == null ) { INSTANCE = new Singleton05(); } } return INSTANCE; } }
从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码
虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。
这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!
双重检查锁(👍推荐使用)想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Singleton06 { /** * 构造方法私有,防止外部实例化 */ private Singleton06 () { } /** * 静态私有变量 * 声明volatile,防止指令重排,导致的空对象异常 */ private static volatile Singleton06 INSTANCE; /** * 对外提供获取对象的静态方法,使用双重检查锁机制,保证同步代码块中的实例化代码只会被执行一次 * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回单例对象 */ public static Singleton06 getInstance () { if (INSTANCE == null ) { synchronized (Singleton06.class) { if (INSTANCE == null ) { INSTANCE = new Singleton06(); } } } return INSTANCE; } }
首先先来看看该方案于前几种的不同点
使用synchronized关键字实现同步代码块 同步前同步后两次判断 使用了volatile关键字 分析在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程
A、B、C 3个线程执行getInstance方法 A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待 A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁 B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象 从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。
volatile但是在整个流程中有一个小小的隐患
INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
①第一步:给 INSTANCE 分配内存空间; ②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE; ③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。
这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。 这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。
在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。
在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。
关于 volatile关键字和指令重排相关 可以参考此处
总结双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!
静态内部类(👍推荐使用)除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Singleton07 { /** * 构造方法私有,防止外部实例化 */ private Singleton07 () { } /** * 提供一个静态内部类,类中声明一个类型为 Singleton07 的静态属性 INSTANCE */ private static class SingletonInstance { private static final Singleton07 INSTANCE = new Singleton07(); } /** * 对外提供获取对象的静态方法, * 外部调用,类名.方法名 Singleton.getInstance() * * @return 返回静态内部类的静态属性 */ public static Singleton07 getInstance () { return SingletonInstance.INSTANCE; } }
分析该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载 结论保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用
枚举(👍推荐使用)那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class SingletonType08 { public static void main (String[] args) { String connection1 = Singleton08.INSTANCE.getConnection(); String connection2 = Singleton08.INSTANCE.getConnection(); System.out.println("connection1 == connection2 : " + (connection1 == connection2)); System.out.println(connection2.hashCode()); System.out.println(connection2.hashCode()); } } enum Singleton08 { /***/ INSTANCE; /**资源对象,此处以字符串示例*/ private String connection = null ; /** * 在私有构造中实例化单例对象 */ Singleton08() { //模拟实例化过程 this .connection = "127.0.0.1" ; } /** * 对外提供获取资源对象的静态方法 */ public String getConnection () { return connection; } }
如上代码是通过枚举来实现单例对象的创建
enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。 枚举类型是线程安全的,并且只会装载一次。
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。