单例模式及线程安全问题

单例模式

单例模式 是 一种 设计模式(框架)(棋谱);

单例 = 单个实例(对象);

某个类,在一个进程中,之应该创建出一个实例(原则上不应该有多个)

使用单例模式,就可以对代码进行一个严格校验和检查;

举例:例如在一个类中需要创建一个对象来,存放管理 5 个G的数据;如果创建 2,3 个......那么就需要 10G 15G 的空间;这样会造成空间的浪费

如何保证只能创建出唯一的对象呢?

java 语法中不能直接做到这一点·,所以需要一些技巧:

1.饿汉模式

"饿汉模式"形容非常急迫;实例在类加载的时候就已经创建了,创建的实际非常早;

代码解析

1)private static Singleton instance = new Singleton();

静态变量,类加载时创建唯一实例( 首次主动使用类时 ),属于类属性,内存中只有一份;如果不加static ,之后每调用一次 Singleton ,变量就会指向新的内存空间;

2)private Singleton() {}

. 私有构造函数,防止外部通过new创建实例;

3)public static Singleton getInstance()

静态方法,提供外部获取唯一实例的途径,让外部 Singleton类型的变量都指向,Singleton内的唯一实例;

2.懒汉模式

在 "饿汉模式" 下,当类首次被主动使用时,JVM 便会将类加载进内存。在此过程中,唯一实例指向的数据,都会一同被加载至内存当中。这意味着,无论后续是否真正需要用到该实例,在类加载阶段,其占用的内存空间就已被分配,实例所涉及的数据也已准备就绪。

如果是"懒汉模式"JVM 就只会先读取一小部分数据,随着用户需要更多,才会继续读取;

只有在首次调用的时候,才会创建实例;

====================================================

单例模式线程安全问题

在多线程中,并发调用 getInstonce 是否是线程安全;

饿汉模式 调用 getInstonce 本质是在:多个线程 读取 同一个变量;是线程安全

懒汉模式的 getInstance 调用中,多线程会有读写操作。线程 A 读 instance 为 null 时创建实例。因 JVM 优化,线程 B 也读 instance 进寄存器,在 A 未将新实例写回主存时,B 寄存器中的 instance 仍为 null,B 也创建实例。最终,两个实例写回主存致 instance 指向不同空间,引发线程不安全。(这就导致实例被 new 了两次)

针对懒汉模式这个线程不安全的问题,我们可以通过加锁来解决;

此时多线程要想使用,就需要抢占锁,第一次执行操作后,创建了实例,后续都是**"读操作"**就不会有线程安全的问题;

针对这个已经没有线程安全问题的代码,仍然是每次调用都先加锁,在解锁,此时,效率就会很低;

加锁意味着会产生阻塞,一旦线程阻塞,就不会有高性能;

(只有在需要加锁的时候才能加锁,不该加锁的时候不能加)

为了提高性能,我们只给在第一创建的创建的时候上锁,接下来都是单纯的读操作;

我们可以看到,我们使用了两个 判断条件相同的 if()语句;在单线程的代码中,编写两个相同的 if 是无意义的;

但在多线程的代码中,这样的代码就是有意义的;看起来是一样的条件,实际上结果是相反的;(第一个 if 判断是否要加锁,第二个 if判断是否要创建对象;)

这样的代码称为 " 双重校验锁 "

===================================================

指令重排序引起的线程安全

(指令重排序也是 jvm 优化的一种方式)

调整原有代码执行顺序,保证逻辑不变的前提下,提高程序的效率;


举例:

自己写的代码顺序,就像买菜顺序一样,如果没有 jvm 优化执行顺序,这样的买菜顺序需要走许多路;

优化后的 买菜顺序;(jvm 主动优化);

懒汉模式中instance = new SingletonLazy(),这行代码实际会被拆分为 3 个 CPU 指令:

分配内存空间: 给新对象开辟一块内存

初始化对象: 调用构造方法,初始化对象的属性等。

赋值引用: 把instance指向 刚分配的内存地址

正常执行顺序是1→2→3,但编译器 / CPU 可能会指令重排为 1→3→2 先赋值引用,再初始化对象,此时这块内存还 初始化; )。

如果在 1→3→2 的执行顺序下:线程A 先执行 3 此时线程B 调用getInstance(); instance 不为空,就可能直接将 还未初始化内存 返回给 线程B ,instance的属性可能会出现全为 "0"的情况;就会出现错误;

volatile关键字可以禁止指令重排 ,强制1→2→3的执行顺序;同时保证内存可见性

相关推荐
木井巳1 天前
【多线程】单例模式
java·单例模式·java-ee
忧郁的Mr.Li1 天前
设计模式--单例模式
javascript·单例模式·设计模式
卷卷的小趴菜学编程1 天前
项目篇----仿tcmalloc的内存池设计(page cache)
c++·缓存·单例模式·tcmalloc·内存池·span cache
萧曵 丶2 天前
懒加载单例模式中DCL方式和原理解析
java·开发语言·单例模式·dcl
萧曵 丶2 天前
单例模式 7 种实现方式对比表
java·单例模式
当战神遇到编程6 天前
图书管理系统
java·开发语言·单例模式
Remember_9936 天前
Java 单例模式深度解析:设计原理、实现范式与企业级应用场景
java·开发语言·javascript·单例模式·ecmascript
春日见7 天前
win11 分屏设置
java·开发语言·驱动开发·docker·单例模式·计算机外设
短剑重铸之日7 天前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式