Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >面试问到DCL失效不知所措

面试问到DCL失效不知所措

作者头像
iiopsd
发布于 2022-12-23 00:56:39
发布于 2022-12-23 00:56:39
35500
代码可运行
举报
文章被收录于专栏:iiopsd技术专栏iiopsd技术专栏
运行总次数:0
代码可运行

背景

最近在学习设计模式的时候看到了单例模式,里面还是有很多内容的,比如双重检查锁方式实现的单例模式,就是一个面试考点,接下来我们就来详细说说。

单例模式

单例模式很简单,就是在构造函数中多了加一个构造函数,访问权限是 private 的就可以了,这个模式是简单,但有时候简单的东西也很容易出问题。在高并发的项目中,每个请求都要创建同一个单例对象。如果没有控制好,创建了多个单例对象,那就会导致业务逻辑混乱,数据一致性校验失败等复杂的问题,而且难以排查。为了解决这一问题,我们可以使用双重检查锁实现单例模式。

单例模式-双重检查锁(DCL, 即 double-checked locking)

实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.hsy.demo;

/**
 * 懒汉单例
 *
 * 优点:懒加载,线程安全,效率较⾼
 * 缺点:实现较复杂
 *
 * @date 2022-04-01 11:08:26
 * @since
 */
public class LazySingletonDCL {
    /**
     * 私有构造函数
     *
     * @param
     * @author Huangshaoyang
     * @date 2022-04-01 11:04:10
     * @since
     */
    private LazySingletonDCL() {
    }

    /**
     * 定义⼀个静态变量指向⾃⼰类型
     *
     * volatile 避免DCL 失效
     */
    private volatile static LazySingletonDCL lazySingleton = null;


    /**
     * 获取实例方法
     *
     * @param
     * @author Huangshaoyang
     * @date 2022-04-01 10:04:33
     * @since
     */
    public static LazySingletonDCL getLazySingleton() {
        // 第⼀重检查是否为 null
        if (lazySingleton == null) {
            // 使⽤ synchronized 加锁
            synchronized (LazySingletonDCL.class) {
                // 第二重检查是否为 null
                if (lazySingleton == null) {
                    lazySingleton = new LazySingletonDCL();
                }
            }
        }
        return lazySingleton;
    }

}

这种实现方式的优点:懒加载,线程安全,效率较⾼

这种实现方式的缺点:实现较复杂

实现原理

这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回单例。

关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。

其中最关键的⼀个点就是 volatile 关键字的使⽤,关于 volatile 的详细介绍可以直接搜索 volatile 关键字即可,有很多写的⾮常好的⽂章,这⾥不做详细介绍。

简单说明⼀下,双重检查锁中使⽤ volatile 的两个重要特性:可⻅性、禁⽌指令重排序

这⾥为什么要使用volatile ?

这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯的步骤:

  1. 在堆内存开辟内存空间
  2. 调⽤构造⽅法,初始化对象
  3. 引⽤变量指向堆内存空间

对应字节码指令如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
9: astore_0
10: monitorenter  #synchronized锁
11: getstatic
14: ifnonnull
17new
20:dup
21:invokespecial
24:putstatic
27: aload_0
28: monitorexit
29: goto

为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重排序,从源码到最终执⾏指令会经历如下流程:

1、源码

2、编译器优化重排序

3、指令级并⾏重排序

4、内存系统重排序

5、最终执⾏指令序列

所以经过指令重排序之后,创建对象的执⾏顺序可能为17、21、24或者17、24、21,因此当某个线程在乱序运⾏17、24、21指令的时候,引⽤变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使用了没有初始化的非空实例,这样的话就会出现异常,这个就是著名的DCL 失效问题。

当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁⽌指令重排序,就可以避免这个问题,⽽且对volatile 修饰的变量的修改对其他任何线程都是可⻅的。

THE END.

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=staxjsob8l25

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
MySQL内置数据库performance_schema详解(七):监视内存使用的表介绍
performance_schema 是 MySQL 数据库中的一个内置的系统数据库,最早从MySQL5.5版本产生,这个数据库主要用于收集和存储与数据库性能相关的统计信息和指标。
小明互联网技术分享社区
2023/12/28
4890
MySQL内置数据库performance_schema详解(七):监视内存使用的表介绍
系统设计中的快速估算技巧
拿到一堆数据,去做架构也好,设计也好,可行性分析也好,工程上需要的是严谨。但是也有很多场景,比如即时的问题争辩和讨论,我们往往需要的是快速、直接的估算,这样的数据显然不需要非常精确,甚至可以说它一定会非常粗略,我们的目标往往只停留在 “量级” 的级别,但是我们依然可以对方案有一个具体的、量化的认知,这比像 “海量”、“高吞吐”、“低延迟” 这类感性的、描述性的表述还是要清晰和有力得多。
四火
2022/07/19
6580
系统设计中的快速估算技巧
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏02支持中文及显示FPS
如果设置FPS为20,这意味着我们命令游戏的每个循环持续1 / 20(0.05)秒。如果循环代码(更新,绘图等)只需要0.03秒,那么我们将等待0.02秒。以上是计算机处理比较快的情况。如果电脑比较差,运行缓慢,一秒钟未必能执行20次循环--- 那么FPS设置成20就成为一个指导意见。
豆约翰
2021/12/31
3820
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏02支持中文及显示FPS
【DB笔试面试625】在Oracle中,如何获取timestamp类型的精度到纳秒级?
秒(s)的单位都有毫秒(ms,millisecond),微秒(μs,microsecond),纳秒(ns,nanosecond),它们之间的换算单位为千进制,1s(秒)=10^3ms(毫秒)=10^6μs(微秒)=10^9ns(纳秒)。
AiDBA宝典
2019/09/29
1.3K0
笔记26 | 总结Android的获取系统时间的几种方法
地址 http://blog.csdn.net/xiangyong_1521/article/details/78396629 目录 1.Calendar 2.Date 3.currentTimeMills 一.使用Calendar获取系统时间 Calendar获取系统时间首先要用Calendar.getInstance()函数获取一个实例,再为该实例设定时区(中国的时区为GMT+8:00),最后使用Calendar.get()函数获取时间的具体信息,如年,月,日,小时,分,秒,星期几。 缺点是获得的这些
项勇
2018/06/19
2.6K0
『Go 内置库第一季:time』
时间的操作在项目中使用的非常频繁,比如说数据库中,经常有时间的操作,比如根据时间进行划分,统计之类的功能。
谢伟
2018/12/12
8870
Java程序运行纳秒级差值计算
可以看到纳秒更加细致的反应除了程序的运行之间,基本上定义一个变量用时100纳秒,咱们可以根据具体的情况进行更为细致的优化,让程序更加的快捷。
红目香薰
2022/11/29
8260
Java程序运行纳秒级差值计算
学习PHP中的高精度计时器HRTime扩展
不知道大家还记得在学校的时候体育测试时老师带的秒表吗?当枪声想起时,我们开始跑步,这时秒表启动,当我们跑过终点后,老师会按下按扭记录我们的成绩,这就是一个典型的定时器的应用。今天我们要学习的内容其实就是和这个体育测验的秒表类似的一个功能扩展,它就是 PHP 的 HRTime 扩展。
硬核项目经理
2021/03/16
1.4K0
Linux|容易迷糊的时间戳事件
起因是在排错的时候,同事说log的时间不对,通过解析时间戳怎么是中国的时间巴拉巴拉的,理论上应该是设备所在的当地时间。
琉璃康康
2024/02/01
6040
Linux|容易迷糊的时间戳事件
Go 语言 time 包常用用法笔记
Go 的时间操作基本上都用 time 包,比 C 的 time 函数和 timeval 等 struct 好用多了。不过 time 包还是有不少用法和其他语言不同的,所以有必要写个笔记记录一下。
amc
2019/07/03
3.6K0
Go 语言 time 包常用用法笔记
ESP8266定时器.上
我上篇文章说了,我要写写ESP8266的定时器。我们这里要加一个微秒:百万分之一秒,10(-6)次方。就使用到计算周期。
云深无际
2022/06/27
4160
ESP8266定时器.上
C语言之Sleep函数
Sleep函数: 功 能: 执行挂起一段时间   用 法: unsigned sleep(unsigned seconds);   注意:   在VC中使用带上头文件#include <windows.h>,在Linux下,gcc编译器中,使用的头文件因gcc版本的不同而不同#include <unistd.h>  在VC中,Sleep中的第一个英文字符为大写的"S" ,在linux下不要大写,在标准C中是sleep, 不要大写,简单的说VC用Sleep, 别的一律使用sleep 在VC中,Sleep()里
互联网金融打杂
2018/04/03
7.8K0
SQL函数 DATENAME
DATENAME函数返回日期/时间值中指定部分的名称(例如“June”)。 结果作为数据类型VARCHAR(20)返回。 如果结果是数字(例如“23”表示当天),它仍然作为VARCHAR(20)字符串返回。 要以整数形式返回此信息,请使用DATEPART。 要返回包含多个日期部分的字符串,请使用TO_DATE。
用户7741497
2022/04/01
1.7K0
【Go 语言社区】Golang语言的time.Sleep
首先:time.sleep单位为:1ns (纳秒) 转换单位: 1纳秒 =1000皮秒 1纳秒 =0.001 微秒 1纳秒 =0.000 001毫秒   1纳秒 =0.000 000 001秒 写无限循环代码的时候老是担心 time.sleep时间过短或者过长的影响 于是乎就测试了下 结果终于安心了,原来这是有瓶颈的。 贴代码: package main import( "fmt" "time" ) func main(){ fmt.Println(i
李海彬
2018/03/20
4K0
【51单片机】十分钟学会定时器&中断¹
🚩write in front🚩  🔎大家好,我是謓泽,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎 🏅2021年度博客之星物联网与嵌入式开发TOP5~2021博客之星Top100~阿里云专家^星级博主~掘金⇿InfoQ创作者~周榜34»总榜1889🏅 🆔本文由 謓泽 原创 CSDN首发🙉如需转载还请通知⚠ 📝个人主页-謓泽的博客_CSDN博客 📃 🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​ 📣系列专栏-【51单片机】系列_謓泽的博客-CSDN博客🎓 ✉️我们并非登上我们所
謓泽
2022/12/12
9820
【51单片机】十分钟学会定时器&中断¹
现代c++中实现精确延时方法总结
这种非常不建议用,懒人做法。不够精确且换种环境系统处理速度不一样可能就是bug来源。
杨永贞
2022/11/21
3.7K0
现代c++中实现精确延时方法总结
Java获取程序执行时间
本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/176
joshua317
2021/10/26
8170
编码标识符缩写建议
编码过程中,对一个标识符(变量、函数或类型)取名时,为了保持代码的简洁,如果标识符过长需要缩写。缩写时,应采用统一的缩写规则,避免含糊不清,目的是为了保持命名统一,减少沟通成本,提升团队研发效率。
恋喵大鲤鱼
2022/05/09
1.2K0
Golang内置包-time
时间离我们仅在咫尺,无论是在编程中时间还是日常生活中对于时间的 记述都是离我们最近的,那么让我们一起来学习一下GoLang中内置包time。
PayneWu
2020/12/18
8760
线程池实现原理
线程池创建线程时,会将线程封装成工作线程 Worker , Worker 在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从 Worker 类的 run()方法里看到这点。
kwan的解忧杂货铺
2024/08/13
860
推荐阅读
相关推荐
MySQL内置数据库performance_schema详解(七):监视内存使用的表介绍
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验