面试问到DCL失效不知所措

本文介绍单例模式及双重检查锁(DCL)的实现方法。通过具体代码示例,解析DCL如何确保线程安全地实现懒加载,并探讨volatile关键字的作用。

背景

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

单例模式

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

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

实现代码如下:

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. 引⽤变量指向堆内存空间

对应字节码指令如下:

9: astore_0
10: monitorenter  #synchronized锁
11: getstatic
14: ifnonnull
17:new
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

### MySQL 数据库常见面试问题 以下是关于 MySQL 数据库的一些常见面试问题及其解答: #### 1. 主从复制的作用是什么? 主从复制的主要作用包括实现 **读写分离** 和提高系统的并发能力,以及提供 **高可用性**。通过设置从数据库作为备份节点,在主数据库发生故障时可以从数据库接管服务,从而减少停机时间并避免数据丢失[^1]。 #### 2. 如何解决大数据量表之间的高效查询问题? 针对两个大表(如一个6亿条记录的表A和一个3亿条记录的表B),可以通过优化 SQL 查询逻辑来提升性能。例如,利用分页查询技术结合索引访问模式,或者调整 WHERE 条件以减少扫描范围。具体方法可能涉及使用覆盖索引、分区表设计或基于内存缓存机制加速检索过程[^2]。 #### 3. Binlog 日志的特点有哪些优缺点? 优点方面,由于只记录改变了数据库状态的操作命令 (DML/DCL),因此相比全量备份方式更加节省磁盘空间;另外它还可以用于审计追踪目的。然而其局限在于无法恢复 SELECT 类型未更改任何实际内容却被执行过的请求信息[^3]。 #### 4. 设置了索引为何仍存在某些场景下不能被有效利用的情况呢? 即使建立了合适类型的索引结构,但在特定条件下仍然可能出现失效现象: - 当 `LIKE` 子句中通配符位于字符串起始位置(`%abc`)时; - 如果组合条件表达式里部分列未能命中相应索引项(比如 OR 连接); - 发生隐含的数据类型转换情形之下; - 对目标字段应用数学运算或是调用了内置函数处理之后再参与比较判断操作之中等等情况均可能导致原有定义好的索引失去效果[^4]. #### 5. 怎样移除已存在的单一或多级复合形式下的不同种类别的索引对象 ? 对于简单唯一约束性质或者是普通二级索引而言可以直接采用如下语法完成删除动作:`ALTER TABLE table_name DROP INDEX index_name;`.而涉及到主键时候则需特别注意一点就是当该主键同时还具备AUTO_INCREMENT属性特性的时候就不能单纯依靠上述指令单独去掉这个特殊标记功能了. --- ```sql -- 删除普通索引 ALTER TABLE 表名 DROP INDEX 索引名称; -- 删除主键索引(前提是不存在 AUTO_INCREMENT) ALTER TABLE 表名 DROP PRIMARY KEY; ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值