环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE
1. 前言
哈喽,小伙伴们,你们好呀,今天我们就不整那枯燥无味的知识点了,偶尔换换口味,我们来玩点高级的;由于很多小伙伴都在给我传递一种负面情绪,今年的工作很难找,我就在想是不是八股文没准备充足啊?于是我就在总结高频笔试题,借此想把整理到的笔试题进行集合式的讨论,不仅帮助大家理解,也能帮助自己加深理解,何乐而不为呢。
2. 环境说明
js
复制代码
环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE
3. 需求分析
手写一个单例?那么首先你就要清楚单例是什么?如果都不清楚何为单例,那无法下手!首先我就带着大家简单回顾一下设计模式之单例模式的相关知识点吧。
3.1 概念
单例模式是java中设计模式里最简单的模式之一,属于创建型模式。这种模式它提供了一种创建对象的最佳方式,在创建对象的过程中,只涉及一个类,且该类负责创建自身对象,并保证只能创建单个对象实例。
总而言之,该模式的主要目的就是确保一个类只能有一个实例存在。
3.2 特点
单例模式特点可总结为以下3点
-
单例类只有一个实例对象。
-
该实例对象必须由单例类本身自行创建。
-
单例类必须提供一个共有的静态方法向外面提供这个实例。
3.3 分类
单例模式可根据实例创建的时机进行分类,可分为[饿汉式、懒汉式]两类。
-
饿汉式:类加载就会创建单实例对象。
-
懒汉式:类加载不会导致该单实例对象被创建,只有在被使用的时候才会创建。
总而言之,分类的意义在于实际运用场景决定,各有各的优缺点。比如懒汉模式,只有在被使用的时候才创建,节省资源,体现了延迟加载的思想;但在并发场景下同一时间被多个线程调用,则很有可能被创建出多个实例,违背唯一实例原则。而恶汉模式,写法上简单,而且在多线程下也能保证唯一实例,但如果外部一直未调用该实例又先实例化了,这部分资源也就白白浪费了。
4. 代码演示
如下我将分别从单例模式的两种分类来实现代码单例,仅供参考:
4.1 懒汉式单例(线程安全)
代码实现如下:
csharp
复制代码
public class LazySingLeton { //私有构造方法 private LazySingLeton() { System.out.println("生成LazySingLeton实例一次"); } //在成员位置创建该类的对象 private static LazySingLeton instance = null; public synchronized static LazySingLeton getInstance() { if (instance == null) { instance = new LazySingLeton(); } return instance; } }
注意:此种写法虽然解决了线程安全问题,但synchronized同步的方法是静态的,会导致进入该方法时JVM会锁定LazySingLeton这个类,锁的粒度太大,大量线程同时访问的时候直接阻塞,性能太低。
接下来,我们还是先写个main函数顺序调用上三遍懒汉式单例测试一波看看。
typescript
复制代码
//测试 public static void main(String[] args) { LazySingLeton.getInstance(); LazySingLeton.getInstance(); LazySingLeton.getInstance(); }
大家可以看到,实例确实只被创建了一次!而非三次。

4.2 恶汉式单例
代码实现如下:
csharp
复制代码
public class NoLazySingLeton { // 私有构造方法 private NoLazySingLeton() { System.out.println("生成NoLazySingLeton实例一次!"); } // 在成员位置创建该类的对象 private static NoLazySingLeton instance = new NoLazySingLeton(); // 对外提供静态方法获取该实例对象 public static NoLazySingLeton getInstance() { return instance; } }
写个main函数顺序调用上三遍恶汉式单例,静待结果:
typescript
复制代码
//测试 public static void main(String[] args) { //调用三遍 NoLazySingLeton.getInstance(); NoLazySingLeton.getInstance(); NoLazySingLeton.getInstance(); }
大家可以看到,实例确实只被创建了一次!而非三次。

对比以上两种写法,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成线程不安全就完全不存在了,这也就是懒汉恶汉式的抉择问题了,重点也就在于你是何应用场景了。
4.3 懒汉式单例(双重检查模式)
最后,我们再来讨论一下,对比上方保证线程安全懒汉式而言,若想做到即解决性能问题又能保证线程安全,那可以这么干,浅浅听我分析,对于 getInstance() 方法来说,绝大部分的操作都是读 操作,读操作是线程安全的,所以没必让每个线程必须持有锁才能调用该方法,我们只需要调整加锁的时机,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,从而达到解决性能消耗的问题。具体代码大家请看如下,我把代码注释都写的明明白白。
csharp
复制代码
public class DoubleLazySingLeton { private static DoubleLazySingLeton instance; // 私有构造方法 private DoubleLazySingLeton() { System.out.println("生成DoubleLazySingLeton实例一次!"); } // 对外提供静态方法获取该对象 public static DoubleLazySingLeton getInstance() { // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例. if (instance == null) { //instance未实例化的时候才加锁 synchronized (DoubleLazySingLeton.class) { // 抢到锁之后再次判断是否为null if (instance == null) { instance = new DoubleLazySingLeton(); } } } return instance; } }
但是话又说回来, 虽然【双重检查模式】能保证性能及线程安全,但是也并非完美无缺,在多线程情况下,可能会有空指针异常的问题,因为JVM在实例化对象的时候会进行优化和指令重排序操作。
若要解决双重检查模式带来空指针异常的问题,你只需要使用[volatile]关键字,因为volatile关键字可以保证可见性和有序性(禁止指令重排序),顾保证了new DoubleLazySingLeton()创建对象实例化过程的顺序性,具体请看如下:
4.4 懒汉式单例(DCL双重校验锁)
具体代码如下:
csharp
复制代码
public class DoubleLazySingLeton { //关键字volatile保证对象实例化过程的顺序性。 private static volatile DoubleLazySingLeton instance; // 私有构造方法 private DoubleLazySingLeton() { System.out.println("生成DoubleLazySingLeton实例一次!"); } // 对外提供静态方法获取该对象 public static DoubleLazySingLeton getInstance() { // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例. if (instance == null) { //instance未实例化的时候才加锁 synchronized (DoubleLazySingLeton.class) { // 抢到锁之后再次判断是否为null if (instance == null) { instance = new DoubleLazySingLeton(); } } } return instance; } }
由于volatile关键字它可以禁止指令重排序来保证一定的有序性,自然就解决了空指针异常的问题。
... ...
ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看我的往期热文推荐哦,每天积累一个奇淫小知识,日积月累下去,你一定能成为令人敬仰的大佬。
「赠人玫瑰,手留余香」,咱们下期拜拜~~
5. 文末💭
我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!
本文介绍了Java中的单例模式,包括概念、特点和分类,如饿汉式和懒汉式。详细讲解了懒汉式线程安全的实现,以及性能优化的双重检查模式(DCL),并指出volatile关键字在解决线程安全和空指针异常问题中的作用。
794

被折叠的 条评论
为什么被折叠?



