Java设计模式之单例模式详细讲解
创始人
2024-04-03 23:17:05
0

设计模式与单例模式

1、什么是单例模式

​ 单例模式是指保证某个类在整个软件系统中只有一个对象实例,并且该类仅提供一个返回其对象实例的方法(通常为静态方法)

2、单例模式的种类

​ 经典的单例模式实现方式一般有五种

2.1 饿汉式

// ①饿汉式:使用静态常量
static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}// 2.类的内部创建对象private final static Singleton instance = new Singleton();// 3.向外部暴露一个静态的公共方法public static Singleton getInstance() {return instance;}
}
// ②饿汉式:使用静态代码块
static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}private static final Singleton instance;// 2.静态代码块实例化static {instance = new Singleton();}// 3.向外部暴露一个静态的公共方法public static Singleton getInstance() {return instance;}
}

​ 饿汉式顾名思义就是迫不及待地加载该类的对象实例,对象实例的加载最早是在类的加载过程中的初始化阶段(即静态引用变量的加载,对应字节码文件中方法的执行),加载过程由JVM保证线程安全。饿汉式会浪费内存,但是随着计算机的发展,内存已经不是问题了,所以使用饿汉式也未尝不可。

JDK源码举例:
在这里插入图片描述
​ 该类位于java.lang包下,首先将构造方法私有化,声明了一个私有的静态变量并且对该变量进行对象实例的创建,再创建一个公有的静态方法返回这个对象实例,这是比较常用的一种实现单例模式的方式。

2.2 懒汉式

// ①懒汉式:线程不安全
static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}private static Singleton instance;// 2.向外部暴露一个静态的公共方法public static Singleton getInstance() {// 3.instance == null时进行实例化if ( instance == null ) {// new Singleton()不是一个原子操作,JVM中会进行大致[创建对象-分配内存-对象初始化]等过程,在这之前instance都为null// 多线程情况下,多个线程同时执行到该位置,线程获取到时间片后会继续执行,就可能创建多个实例instance = new Singleton();}return instance;}
}
// ②懒汉式:线程安全(方法上添加 synchronized 关键字)
static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}private static Singleton instance;// 2.向外部暴露一个静态的公共方法, synchronized 保证线程安全public static synchronized Singleton getInstance() {// 3.instance == null时进行实例化if ( instance == null ) {instance = new Singleton();}return instance;}
}

懒汉式就是在创建对象实例前先判断是否已经创建,但是由于对象实例的创建并不是一个原子过程,所以会出现线程安全问题,可以在方法上添加synchronized解决,当然会牺牲一定的性能。基于以上原因,不推荐使用懒汉式的方式实现单例模式。

​ 如何证明对象实例的创建不是一个原子操作?字节码指令可以从侧面证明

// Java源码
public class Test {public Test getTest() {return new Test();}
}

在这里插入图片描述
红框1的位置有三条字节码指令,这还只是字节码的层面,再往低层还会有更多的步骤,所以很明显对象实例的创建不是一个原子操作

2.3 双重检查锁

static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}// 2.volatile保证多线程下的可见性private static volatile Singleton instance;// 3.向外部暴露一个静态的公共方法public static Singleton getInstance() {// 3.非空判断if ( instance == null ) {// 4.同步代码块synchronized (Singleton.class) {// 5.再次非空判断(保证多线程下的单例)if ( instance == null ) {instance = new Singleton();}}}return instance;}
}

双重检查锁是最复杂的一种单例模式实现方式,我把它拆分成三个问题:

​ ① 为什么synchronized不加到方法上?

​ 如果添加到方法上,两次非空判断就没有必要了,一次就够了,就转化成了懒汉式(线程安全),这种方式效率不高,因为每次调用都需要获取锁和释放锁。

​ ② 为什么要做两次非空判断?

​ 之前也提到过:对象实例的创建不是一个原子操作。线程安全问题也是出在这一过程中的,解决方案就是添加synchronized关键字,但是添加到方法上效率又太低了;

​ 既然问题是出现在对象实例创建的过程中,那么只对这一段代码进行同步操作(加锁对象就是当前的Class对象,因为对象实例只有一个);

​ 第一层的非空判断是为了如果对象实例已经创建完成了,就不需要再次进入同步代码块了,直接返回创建好的对象实例即可。

​ ③ 为什么要加volatile?
在这里插入图片描述
根据对象实例创建的字节码指令可以看出对象实例的创建大致分为三步:

​ ① 在堆内存中分配对象内存

​ ② 调用方法,执行对象实例的初始化

​ ③ 将对象引用赋给静态变量

大家应该对JMM模型和happens-before有所了解,简单来说JMM模型是对编译器和处理器的约束,happens-before是对开发者的约束。

编译器和处理器在实际运行时,为了执行效率可能会对指令进行重排序的操作,虽然单线程中不会影响执行结果,但是如果是多线程就会出现问题。

像对象实例创建过程的三条指令中②③就有可能会被优化为③②,但是①一定会先执行,因为②③依赖于①,此时执行顺序为①③②,其他线程就会获取到一个未初始化的对象,导致执行出错。

而volatile关键字的语义包含两个:

​ ① 保证可见性

​ ② 禁止指令重排序(所以添加volatile后,执行顺序就是①②③了)

​ JDK源码举例:
在这里插入图片描述
该类是位于java.lang包下的System类,经典的双重检查锁实现方式。

2.4 静态内部类

static class Singleton {// 1.构造器私有化,其他类不能newprivate Singleton() {}// 2.静态内部类,Singleton类加载的时候不会加载内部类,只有用到内部类时才回去加载内部类(保证懒加载)private static class SingletonInstance {private static final Singleton instance = new Singleton();}// 3.向外部暴露一个静态的公共方法,此时会装载SingletonInstance,类装载时是线程安全的(保证线程安全)public static Singleton getInstance() {return SingletonInstance.instance;}
}

​ 这是一种很巧妙的方式,相对于饿汉式来说,不需要在类的初始化阶段就创建对象实例,只有在需要(即调用getInstance()方法)的时候才会进行对象实例创建,线程安全也由JVM保证。

​ JDK源码举例:
在这里插入图片描述

​ 上图是java.lang.Short源码中的内部类,将常用的整数保存到缓存池当中;下图是访问缓存池中的整数。类似的还有java.lang.Integer,java.lang.Long等包装类。

在这里插入图片描述

2.5 枚举

// enum实际上是extends抽象类java.lang.Enum
enum Singleton {instance
}

字节码反编译看下:
在这里插入图片描述

enum关键字修饰的类实际上继承了java.lang.Enum 在这里插入图片描述
枚举类中声明的实例实际上是public static final修饰的常量
在这里插入图片描述
上图为枚举类中方法的字节码指令,也就是类的初始化阶段需要执行的逻辑(即将静态变量,静态代码块整合到一块)。

​ 红框1:创建Singleton枚举类对象实例,实际上调用了java.lang.Enum类的构造器(即方法),构造器参数是(“INSTANCE”, 0),可以通过ldc #7和iconst_0看出来;对象实例创建完成后,将实例引用赋给INSTANCE常量。

​ 红框2:将上一步创建的对象实例引用保存到枚举类内部数组$VALUES中,外部可以通过values()方法返回所有的枚举对象引用;数组的创建是在iconst_1和anewarray,意思是创建一个长度为1的引用类型数组。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...