单例模式之双重校验锁

news/2024/11/9 9:57:43/

参考文章: https://blog.csdn.net/weixin_44471490/article/details/108929289
解释得很好,一步一步分析为什么这样 写

单例模式

单例即单实例,只实例出来一个对象。

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

如何实现单例:

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

应用场景:

用于全局类对象在多个地方被使用,且对象状态是全局的场景下

单例模式优点:

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

写法

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

饿汉模式

就是在加载类的时候直接new一个对象,后面直接使用即可

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

public class Singleton {// 私有静态实例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才实例化,该方法不是线程安全的

静态内部类

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

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

饿汉模式和静态内部类实现单例模式的优点是写法简单,缺点是不适合复杂对象的创建。对于涉及复杂对象创建的单例模式,比较优雅的实现方式是懒汉模式,但是懒汉模式是非线程安全的,下面就讲一下懒汉模式的升级版——双重构校验锁模式(双重构校验锁是线程安全的)。

双重校验锁

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

单线程懒汉模式

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

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;
}

解决问题:加锁

为了解决问题1, 可以对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() {synchronized(Lock2Singleton.class) {if(INSTANCE == null) {INSTANCE = new Lock2Singleton();}}return INSTANCE;}
}

优化后对代码 if(INSTANCE == nul) 和INSTANCE = new Lock2Singleton()加锁

每个线程进入方法后先加锁,保证不会有多个线程同时用于创建对象,保证唯一性

如何解决问题2?
需要禁止指令重排,加volatile关键字, 防止得到未初始化对象

public class Lock2Singleton{private volatile static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {synchronized(Lock2Singleton.class) {if (INSTANCE == null) {INSTANCE = new Lock2Singleton();}}}return INSTANCE;
}

从功能上是完整的,在性能上还可以优化:
在synchronized代码块上,加锁是非常影响效率的,如果INSTANCE不为null,可以先判断INSTANCE是否为null,防止进入synchronized代码块

public class Lock2Singleton{private vloatile static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {if (INSTANCE == null) {synchronized(Lock2Singleton.class) {if (INSTANCE == null{INSTANCE = new Lock2Singleton();}}}return INSTANCE;}
}

在 synchronized 代码块之外再加一个 if 判断,这样,当 INSTANCE 已经存在时,线程先判断不为null,然后直接返回,避免了进入 synchronized 同步代码块。

思考里面的null判断是否可以去掉? ——不可以,因为只对 只对 INSTANCE = new Lock2Singleton() 加了锁,,防止第二个if和new操作间有别的线程进来,引起问题1

两次校验,一次不能少!

总结

单例模式双重校验模式完整代码:

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时仍然去竞争锁。
为什么加 volatile ?
加 volatile 是为了禁止指令重排序,也就是为了解决问题②,即避免某个线程获取到其他线程没有初始化完全的对象。

http://www.ppmy.cn/news/275999.html

相关文章

MSE播放fragmented mp4 问题记录

一、在使用MSE 播放视频的时候发现firfox能播放,chrome 不能播放 原因:两边要求的fragmented mp4的格式要求不一样 , 参照Transcoding assets for Media Source Extensions - Web APIs | MDN 用ffmpeg 转成 对应的格式 firefox ffmpeg -i mp4-264.mp4 -movflags f…

前端-JavaScript异步编程async函数,await

了解async和await之前得先明白什么是同步函数,什么是异步函数。 异步 一个任务连续的执行就叫做同步。如果将任务为分两步执行,执行完第一步,转而执行其它任务,等做好了准备,再回过头执行第二步,这种不连…

IDEA代码提示设置

1. 打开File -> setting -> Editor -> Live Templates 2. 点击中间框框中的右侧""号,选择 Template Group, 命名为MyGroup(随便起名字) 3. 选中 MyGroup 点击右侧""号,选择Live Template Abbreviation 快捷提示 Description 描述 Template tex…

哪个计算机无法做到双屏显示,笔记本电脑怎么实现双屏显示不同的内容

笔记本电脑怎么实现双屏显示不同的内容 2018-09-16 看到网上许多的教材,自己实验过几次,但都没有成功,今日再一次实验,仔细分析,认真摆弄,终于成功了,现将结果告诉大家,希望你少走弯…

双屏显示html vga,官方数据:一台计算机连接到两台显示器,双屏显示(VGA,HDMI)指南...

一台计算机连接两台显示器,双屏显示介绍了双屏显示的原始要求。具有一台显示器的计算机应该是最常见的组合。我们的日常工作和娱乐基本上就是这种结合。但是通过这种用法,当您打开多个窗口时,一个显示器会变得非常拥挤,尤其是在执…

winform 自定义控件属性在属性面板中显示的问题

我们做了自定义控件&#xff0c;在工具箱里拖出来的时候&#xff0c;想要直接在属性面板中直接编辑控件的自定义属性 我们可以用如下标签 代码 <!--Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->[Browsable(tru…

layui 卡片式列表_笔记-layui选项卡

/*** 注意:选项卡 依赖 element 模块,否则无法进行功能性操作** 相关样式* layui-tab 代表一个选项卡* |-layui-tab-brief 代表简介风格的选项卡* |-layui-tab-card 代表卡片风格的选项卡* |- layui-tab-title 用于包裹选项卡的导航列表* |— layui-this 设置默认选中的导航按…

python 评分卡_评分卡原理及Python实现

信用风险计量模型可以包括跟个人信用评级&#xff0c;企业信用评级和国家信用评级。人信用评级有一系列评级模型组成&#xff0c;常见是A卡&#xff08;申请评分卡&#xff09;、B卡&#xff08;行为模型&#xff09;、C卡&#xff08;催收模型&#xff09;和F卡&#xff08;反…