李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
28.并发编程之DCL问题
Leefs
2022-11-01 PM
1166℃
0条
[TOC] ### 一、概述 **定义:如果一个类始终只能创建一个实例,那么这个类被称为单例类,这种设计模式被称为单例模式。** 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时**确保只有单个对象被创建**。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。 注意: 1. 单例类**只能有一个实例**。 2. 单例类必须**自己创建自己的**唯一实例。 3. 单例类必须**给所有其他对象提供**这一实例。 *更详细的可以看之前推送的文章:[单例模式简介](https://lilinchao.com/archives/633.html)* ### 二、懒汉式单例 为了在多线程环境下保护懒汉式,需要加上 synchronized 锁 ```java public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } return INSTANCE; } } ``` **说明** 多线程同时调用`getInstance()`, 如果不加synchronized锁, 此时两个线程同时判断INSTANCE为空, 此时都会`new Singleton()`, 此时就不再符合单例模式。所以要加锁,防止多线程操作共享资源造成的安全问题。 同时,上面代码的效率也存在很大问题,当成功创建一个单例对象后,又来一个线程在执行获取锁时,还是会加锁,再次进行判断`INSTANCE==null`,此时`INSTANCE`肯定不为null,然后就返回刚才创建的INSTANCE。这样做会严重影响性能。 #### 双重检查锁优化 ```java public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { if(INSTANCE == null) { // t2 // 首次访问会同步,而之后的使用没有 synchronized synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } ``` **以上的实现特点是** - 懒惰实例化 - 首次使用 `getInstance()` 才使用 synchronized 加锁,后续使用时无需加锁 - 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外 ### 三、代码存在的问题 但在多线程环境下,上面的代码是有问题的,`getInstance` 方法对应的字节码为: ```java 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 // 判断是否为空 // ldc是获得类对象 6: ldc #3 // class cn/itcast/n5/Singleton // 复制操作数栈栈顶的值放入栈顶, 将类对象的引用地址复制了一份 8: dup // 操作数栈栈顶的值弹出,即将对象的引用地址存到局部变量表中 // 将类对象的引用地址存储了一份,是为了将来解锁用 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 // 新建一个实例 17: new #3 // class cn/itcast/n5/Singleton // 复制了一个实例的引用 20: dup // 通过这个复制的引用调用它的构造方法 21: invokespecial #4 // Method "
":()V // 最开始的这个引用用来进行赋值操作 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn ``` 其中 - 17 表示创建对象,将对象引用入栈 // new Singleton - 20 表示复制一份对象引用 // 引用地址 - 21 表示利用一个对象引用,调用构造方法 - 24 表示利用一个对象引用,赋值给 static INSTANCE 也许 jvm 会优化为:先执行 24,再执行 21。 如果两个线程 t1,t2 按如下时间序列执行: ![28.并发编程之DCL问题01.jpg](https://lilinchao.com/usr/uploads/2022/11/1613151111.jpg) - 关键在于 0: getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 - 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排。 - 注意在 JDK 5 以上的版本的 volatile 才会真正有效 ### 四、volatile解决方案 ```java public final class Singleton { private Singleton() { } private static volatile Singleton INSTANCE = null; public static Singleton getInstance() { // 实例没创建,才会进入内部的 synchronized代码块 if (INSTANCE == null) { synchronized (Singleton.class) { // t2 // 也许有其它线程已经创建实例,所以再判断一次 if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } ``` 字节码上看不出来 volatile 指令的效果 ```java // -------------------------------------> 加入对 INSTANCE 变量的读屏障 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/Singleton 8: dup 9: astore_0 10: monitorenter -----------------------> 保证原子性、可见性 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/Singleton 20: dup 21: invokespecial #4 // Method "
":()V 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; // -------------------------------------> 加入对 INSTANCE 变量的写屏障 27: aload_0 28: monitorexit ------------------------> 保证原子性、可见性 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn ``` 如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点: - 可见性 - 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中 - 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据 - 有序性 - 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 - 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 - 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性 加上`volatile`之后, 保证了`指令的有序性`, 不会发生指令重排, 21就不会跑到24之后执行了 ![28.并发编程之DCL问题02.jpg](https://lilinchao.com/usr/uploads/2022/11/327104284.jpg) - synchronized 既能保证原子性、可见性、有序性,其中有序性是在该共享变量完全被synchronized 所接管(包括共享变量的读写操作),上面的例子中synchronized 外面的 if (INSTANCE == null) 中的INSTANCE读操作没有被synchronized 接管,因此无法保证INSTANCE共享变量的有序性(即不能防止指令重排)。 - 对共享变量加volatile关键字可以保证可见性和有序性,但是不能保证原子性(即不能防止指令交错)。 *附文章参考来源* *《黑马程序员之并发编程》*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2544.html
上一篇
27.并发编程之volatile原理
下一篇
29.并发编程之happens-before规则
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Stream流
Elastisearch
Golang基础
随笔
数学
Python
JavaWEB项目搭建
Zookeeper
国产数据库改造
FastDFS
字符串
高并发
SpringBoot
Java
Docker
Spark Core
递归
CentOS
GET和POST
SQL练习题
Linux
Java阻塞队列
Kibana
LeetCode刷题
前端
JavaSE
Git
Spark
Jquery
Map
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭