单例模式之「双重校验锁」

单例模式之「双重校验锁」

单例模式

  • 单例即单实例,只实例出来一个对象。
  • 一般在创建一些管理器类、工具类的时候,需要用到单例模式,比如JDBCUtil 类,我们只需要一个实例即可(多个实例也可以实现功能,但是增加了代码量且降低了性能)。

如何实现单例

  • 将构造方法私有化
  • 提供一个全局唯一获取该类实例的方法帮助用户获取类的实例

应用场景

  • 主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。

单例模式的优点

  • 单例模式为系统资源的优化提供了很好的思路,频繁创建和销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。

单例模式的写法

  • 饿汉模式
  • 懒汉模式
  • 静态内部类
  • 双重校验锁

饿汉模式

  • 顾名思义,饿汉模式就是加载类的时候直接new一个对象,后面直接用即可。

  • 饿汉模式指在类中直接定义全局的静态对象的实例并初始化,然后提供一个方法获取该实例对象。

  • 代码如下:

  • public class Singleton {// 使用static修饰,类加载的时候new一个对象private static Singleton INSTANCE = new Singleton();// 构造器私有化private Singleton() {}public static Singleton getInstance() {return INSTANCE;}
    }
    

懒汉模式

  • 顾名思义,懒汉模式就是加载类的时候只声明变量,不new对象,后面用到的时候再new对象,然后把对象赋给该变量。

  • 定义一个私有的静态对象INSTANCE,之所以定义INSTANCE为静态,是因为静态属性或方法是属于类的,能够很好地保障单例对象的唯一性;

  • 然后定义一个静态方法获取该对象,如果对象为null,则 new 一个对象并将其赋值给INSTANCE。

  • 代码如下:

  • public class Singleton {private static Singleton INSTANCE;// 构造器私有化private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
    }
    
饿汉模式和懒汉模式的区别在于
  • 饿汉模式是在类加载时将其实例化的,在饿汉模式下,在Class Loader完成后该类的实例便已经存在于JVM中了,即,在getInstance方法第一次被调用前该实例已经存在了,new对象的操作不在getInstance方法内。
  • 而懒汉模式在类中只是定义了变量但是并未实例化,实例化的过程是在获取单例对象的方法中实现的,即,在getInstance方法第一次被调用后该实例才会被创建,new对象的操作在getInstance方法内。
  • 此外注意:饿汉模式的实例在类加载的时候已经存在于JVM中了,因此是线程安全的;懒汉模式通过第一次调用getInstance才实例化,该方法不是线程安全的(后面讲怎么优化)

静态内部类

  • 静态内部类通过在类中定义一个静态内部类,将对象实例的定义和初始化放在内部类中完成,我们在获取对象时要通过静态内部类调用其单例对象。

  • 之所以这样设计,是因为类的静态内部类在JVM中是唯一的,这很好地保障了单例对象的唯一性。
    静态内部类的单例实现方式同样是线程安全的。

  • 代码如下:

  • public class Singleton {private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton(){}public static final Singleton getInstance(){return SingletonHolder.INSTANCE;}
    }
    
  • 饿汉模式和静态内部类实现单例模式的优点是写法简单,缺点是不适合复杂对象的创建。对于涉及复杂对象创建的单例模式,比较优雅的实现方式是懒汉模式,但是懒汉模式是非线程安全的,下面就讲一下懒汉模式的升级版——双重构校验锁模式(双重构校验锁是线程安全的)。

双重构校验锁

  • 饿汉模式是不需要加锁来保证单例的,而懒汉模式虽然节省了内存,但是却需要使用锁来保证单例,因此,双重校验锁就是懒汉模式的升级版本。

单线程懒汉模式实现

  • 普通的懒汉模式在单线程场景下是线程安全的,但在多线程场景下是非线程安全的。

  • 先来看看普通的懒汉模式实现:

  • public class Singleton {private static Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
    }
    

单线程懒汉模式的问题

  • 上面这段代码在单线程环境下没有问题,但是在多线程的情况下会产生线程安全问题。

  • 在多个线程同时调用getInstance方法时,由于方法没有加锁,可能会出现以下情况

  • ① 这些线程可能会创建多个对象

  • ② 某个线程可能会得到一个未完全初始化的对象

  • 为什么会出现以上问题?

  • 对于 ① 的情况解释如下:

  • public static Singleton getInstance() {if (INSTANCE == null) {/*** 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后还没来得及执行INSTANCE = new Singleton()* 此时线程B进来,if判断后INSTANCE为null,且执行完INSTANCE = new Singleton()* 然后,线程A接着执行,由于之前if判断INSTANCE为null,于是执行INSTANCE = new Singleton()重复创建了对象*/INSTANCE = new Singleton();}return INSTANCE;
    }
    
  • 对于 ② 的情况解释如下:

  • public static Singleton getInstance() {if (INSTANCE == null) {/*** 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后开始执行 INSTANCE = new Singleton()* 但是注意,new Singleton()这个操作在JVM层面不是一个原子操作**(具体由三步组成:1.为INSTANCE分配内存空间;2.初始化INSTANCE;3.将INSTANCE指向分配的内存空间,* 且这三步在JVM层面有可能发生指令重排,导致实际执行顺序可能为1-3-2)** 因为new操作不是原子化操作,因此,可能会出现线程A执行new Singleton()时发生指令重排的情况,* 导致实际执行顺序变为1-3-2,当执行完1-3还没来及执行2时(虽然还没执行2,但是对象的引用已经有了,* 只不过引用的是一个还没初始化的对象),此时线程B进来进行if判断后INSTANCE不为null,* 然后直接把线程A new到一半的对象返回了*/INSTANCE = new Singleton();}return INSTANCE;
    }
    

解决问题:加锁

  • 为了解决问题 ①,我们可以对 getInstance() 这个方法加锁。

  • public class Singleton {private static Singleton INSTANCE;private Singleton() {}public static synchronized Singleton getInstance() {  // 加锁if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
    }
    
  • 仔细看,这里是粗暴地对整个 getInstance() 方法加锁,这样做代价很大,因为,只有当第一次调用 getInstance() 时才需要同步创建对象,创建之后再次调用 getInstance() 时就只是简单的返回成员变量,而这里是无需同步的,所以没必要对整个方法加锁。

  • 由于同步一个方法会降低上百倍甚至更高的性能, 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。

  • 所以可以只对方法的部分代码加锁!

  • public class Lock2Singleton {private static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {// 因为INSTANCE是静态变量,所以给Lock2Singleton的Claa对象上锁synchronized(Lock2Singleton.class) {        // 加 synchronizedif (INSTANCE == null) {INSTANCE = new Lock2Singleton();}}return INSTANCE;}
    }
    
  • 优化后的代码选择了对 if (INSTANCE == null) 和 INSTANCE = new Lock2Singleton()加锁

  • 这样,每个线程进到这个方法中之后先加锁,这样就保证了 if (INSTANCE == null) 和 INSTANCE = new Lock2Singleton() 这两行代码被同一个线程执行时不会有另外一个线程进来,由此保证了创建的对象是唯一的。

  • 对象的唯一性保证了,也就是解决了问题①,但是如何解决问题②呢?虽然加了 synchronized,但是 synchronized 是不能禁止指令重排的,也就是说,INSTANCE = new Lock2Singleton(); 这行代码在 JVM 层面还是有可能发生 1-3-2 的现象,那要怎么保证绝对的1-2-3顺序呢,也就是禁止指令重排序,答案是加 volatile 关键字。

  • public class Lock2Singleton {private volatile static Lock2Singleton INSTANCE; // 加 volatileprivate Lock2Singleton() {}public static Lock2Singleton getSingleton() {synchronized(Lock2Singleton.class) {         // 加 synchronizedif (INSTANCE == null) {INSTANCE = new Lock2Singleton();}}return INSTANCE;}
    }
    
  • 这样总可以解决问题 ① 和 ② 了吧,然而你以为这就结束了吗?NO!这段代码从功能层面来讲确实是已经结束了,但是性能方面呢?是不是还有可以优化的地方?

  • 答案是:有!!

  • 值得优化的地方就在于 synchronized 代码块这里。每个线程进来,不管三七二十一,都要先进入同步代码块再说,如果说现在 INSTANCE 已经不为null了,那么,此时当一个线程进来,先获得锁,然后才会执行 if 判断。我们知道加锁是非常影响效率的,所以,如果 INSTANCE 已经不为null,是不是就可以先判断,再进入 synchronized 代码块。如下

  • public class Lock2Singleton {private volatile static Lock2Singleton INSTANCE;    // 加 volatileprivate Lock2Singleton() {}public static Lock2Singleton getSingleton() {if (INSTANCE == null) {                         // 双重校验:第一次校验synchronized(Lock2Singleton.class) {        // 加 synchronizedif (INSTANCE == null) {                 // 双重校验:第二次校验INSTANCE = new Lock2Singleton();}}}return INSTANCE;}
    }
    
  • 在 synchronized 代码块之外再加一个 if 判断,这样,当 INSTANCE 已经存在时,线程先判断不为null,然后直接返回,避免了进入 synchronized 同步代码块。

  • 那么可能又有人问,好了,我明白了在 synchronized 代码块外加一个 if 判断,是不是就意味着里面的那个 if 判断可以去掉?

  • 当然不可以!!

  • 如果把里面的 if 判断去掉,就相当于只对 INSTANCE = new Lock2Singleton() 这一行代码加了个锁,只对一行代码加锁,那你岂不是加了个寂寞(加锁的目的就是防止在第二个if判断和new操作之间有别的线程进来!!),结果还是会引起问题①。

  • 所以,两次校验,一次都不能少!!

总结

  • 最终,单例模式双重校验锁模式的完整代码实现如下:

  • public class Lock2Singleton {private volatile static Lock2Singleton INSTANCE;    // 加 volatileprivate Lock2Singleton() {}public static Lock2Singleton getSingleton() {if (INSTANCE == null) {                         // 双重校验:第一次校验synchronized(Lock2Singleton.class) {        // 加 synchronizedif (INSTANCE == null) {                 // 双重校验:第二次校验INSTANCE = new Lock2Singleton();}}}return INSTANCE;}
    }
    
  • 过程如下:

  • 判断 INSTANCE 是否为null,检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量;

  • 不为null,直接返回,不用去竞争锁

  • 为null,获取锁,然后再次判断(虽然已经判断过,但是在第一个if和synchronized之间仍有可能被另外线程插入导致第一个if判断为null时,当进入同步代码块之后再次判断时已经不为null了,所以需要再次判断)

  • 是否为null

  • 为null,创建并返回

  • 不为null,直接直接返回

  • 为什么是双重校验 ?

  • 第二次校验是为了解决问题①,即避免多个线程重复创建对象。

  • 第一次校验是为了提高效率,避免 INSTANCE 不为null时仍然去竞争锁。

  • 为什么加 volatile ?

  • 加 volatile 是为了禁止指令重排序,也就是为了解决问题②,即避免某个线程获取到其他线程没有初始化完全的对象。

-----------------------------------------------------------------------------------

offer突击训练营简介:

1:针对不知道怎么面试,面试没有信心的小伙伴,我们会给你一个offer保障。

2:我们会监督你15-20天内把面试体系技术点掌握至少7成,这样足够你去找到满意的工作了。

3:我们是面向面试学习指导,不会带你们去写代码,会把项目真实开发的迭代过程和技术细节如何实现业务功能都详细教清楚,你能在面试中流畅表达清楚就行了,项目经验你不用担心(技术老师提供的真实项目经验肯定拿的出手),自己学和别人带着系统学,效率完全不一样。

详情请点击这里:offer突击训练营,给你一个offer的保障,求职跳槽的看过来!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/107697.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(ubuntu) 安装JDK

文章目录 前言参看java版本的命令:安装jdk命令安装jps关闭防火墙:查看端口占用:(坑)ubuntu上Mysql默认标明 区分大小写 前言 提示:常以为人是一个容器,盛着快乐,盛着悲哀。但是人不…

使用 Tkinter Canvas 小部件添加放大镜功能?

一、说明 据我所知,内置的 Tkinter Canvas 类比例不会自动缩放图像。如果您无法使用自定义小部件,则可以缩放原始图像并在调用缩放函数时将其替换在画布上。 二、实现图像放大镜技术细节 我如何将放大和缩小添加到以下脚本中,我想将其绑定到…

初阶数据结构-常见的排序算法

排序 排序的概念常见的排序算法常见排序算法的实现数组的打印 插入排序直接插入排序的实现希尔排序( 缩小增量排序 )希尔排序的实现 交换排序冒泡排序冒泡排序的实现选择排序选择排序的实现堆排序堆排序的实现快速排序快速排序非递归 归并排序归并排序的递归实现归并排序的非递…

Vue 3.0 使用的 diff 算法相比 Vue 2.0 中的双端比对有什么优势?

1、最长递增子序列算法 Vue 3.0 的 diff 算法采用了最长递增子序列算法,能够减少不必要的 DOM 操作,提升性能。 2、静态标记 Vue 3.0 中,编译器会对静态节点进行标记,在更新时可以直接跳过这些静态节点,减少 DOM 操作…

多个子div在父中垂直居中

在一个div下&#xff0c;有多个子div&#xff0c;且子div都是水平垂直居中 <template><div><div class"far"><!-- 注意需要多包裹一层 --><div><div class"son1">1</div><div class"son2">222…

MATLAB-文件自动批量读取文件,并按文件名称或时间顺序进行数据处理

我在处理文件数据时&#xff0c;发现一个一个文件处理效率太低&#xff0c;因此学习了下MATLAB中自动读取特定路径下文件信息的程序&#xff0c;并根据读取信息使用循环进行数据处理&#xff0c;提高效率&#xff0c;在此分享给大家这段代码并给予一些说明&#xff0c;希望能为…

Open3D 进阶(13)使用PCA将点云投影到主成分空间

目录 一、算法原理<font color="#dd00dd">1、三维点云投影二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。爬虫网站自重。 一、算法原理 1、三维点云投影 p r o j

IDEA初始配置

1. 详细设置 安装完IDEA之后的简单配置。 1.1 如何打开详细配置界面 1、显示工具栏 2、选择详细配置菜单或按钮 1.2 系统设置 1、默认启动项目配置 启动IDEA时&#xff0c;默认自动打开上次开发的项目&#xff1f;还是自己选择&#xff1f; 如果去掉Reopen projects on …

“Linux免除系统交互操作方法、expect自动化交互工具” 及 “SSH批量修改主机密码脚本”

一、Linux系统免除交互操作方法 1、EOF多文本输入 案例&#xff1a;为机器磁盘进行分区并实现挂载&#xff0c;免交互式操作&#xff0c;如何实现&#xff1f; #!/bin/bash fdisk /dev/sdb <<EOF n p 1 wq EOFmkfs.xfs /dev/sdb1 && mkdir -p /data &&am…

《动手学深度学习 Pytorch版》 8.6 循环神经网络的简洁实现

import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab d2l.load_data_time_machine(batch_size, num_steps)8.6.1 定义模型 num_hiddens 256 rnn_layer nn.RNN(len(voca…

【python高级】设计模式、类工厂、对象工厂

一、说明 最近试着读Design pattern&#xff0c; 不过有些概念实在太抽象了&#xff0c; 整理一下自己所学抽象工厂的精神&#xff0c;就是要有abstract class&#xff08;not implement&#xff09;&#xff0c;而所有不同种类的对象&#xff0c;都是继承这个abstract class&a…

Linux命令(94)之history

linux命令之history 1.history介绍 linux命令history会记录并显示用户所执行过的所有命令&#xff0c;也可以对其命令进行修改和删除操作。 2.history用法 history [参数] history参数 参数说明-a将当前会话的历史信息追加到历史文件(.bash_history)中-c删除所有条目从而清…

【云计算网络安全】DDoS 攻击类型:什么是 ACK 洪水 DDoS 攻击

文章目录 一、什么是 ACK 洪水 DDoS 攻击&#xff1f;二、什么是数据包&#xff1f;三、什么是 ACK 数据包&#xff1f;四、ACK 洪水攻击如何工作&#xff1f;五、SYN ACK 洪水攻击如何工作&#xff1f;六、文末送书《AWD特训营》内容简介读者对象 一、什么是 ACK 洪水 DDoS 攻…

猫头虎博客带您使用Markdown编辑器

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

BAT026:删除当前目录及子目录下的空文件夹

引言&#xff1a;编写批处理程序&#xff0c;实现批量删除当前目录及子目录下的空文件夹。 一、新建Windows批处理文件 参考博客&#xff1a; CSDNhttps://mp.csdn.net/mp_blog/creation/editor/132137544 二、写入批处理代码 1.右键新建的批处理文件&#xff0c;点击【编辑…

代码随想录训练营二刷第五十八天 | 583. 两个字符串的删除操作 72. 编辑距离

代码随想录训练营二刷第五十八天 | 583. 两个字符串的删除操作 72. 编辑距离 一、583. 两个字符串的删除操作 题目链接&#xff1a;https://leetcode.cn/problems/delete-operation-for-two-strings/ 思路&#xff1a;定义dp[i][j]为要是得区间[0,i-1]和区间[0,j-1]所需要删除…

leetcode做题笔记179. 最大数

给定一组非负整数 nums&#xff0c;重新排列每个数的顺序&#xff08;每个数不可拆分&#xff09;使之组成一个最大的整数。 注意&#xff1a;输出结果可能非常大&#xff0c;所以你需要返回一个字符串而不是整数。 示例 1&#xff1a; 输入&#xff1a;nums [10,2] 输出&am…

Linux实现无需手动输入密码的自动化SSH身份验证

SSH密钥身份验证是一种安全的方式&#xff0c;使您能够在无需手动输入密码的情况下连接到远程服务器。以下是如何设置SSH密钥身份验证&#xff0c;以便您的脚本能够自动运行&#xff1a; 步骤 生成SSH密钥对: 在您的本地系统上生成SSH密钥对。如果您尚未生成&#xff0c;请使用…

JimuReport 积木报表 v1.6.4 稳定版本正式发布 — 开源免费的低代码报表

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

【前端设计模式】之备忘录模式

备忘录模式是一种行为设计模式&#xff0c;它允许在不破坏封装性的前提下捕获和恢复对象的内部状态。在前端开发中&#xff0c;备忘录模式可以用于保存和恢复用户界面的状态&#xff0c;以及实现撤销和重做功能。 备忘录模式特性&#xff1a; 封装了对象的状态&#xff1a;备…