单例模式中的线程安全问题

简介: 单例模式中的线程安全问题

🍈一. 使用多线程需要考虑的因素

提高效率: 使用多线程就是为了充分利用CPU资源,提高任务的效率

线程安全: 使用多线程最基本的就是保障线程安全问题

所以我们在设计多线程代码的时候就必须在满足线程安全的前提下尽可能的提高任务执行的效

故:

加锁细粒度化:加锁的代码少一点,让其他代码可以并发并行的执行

🍬考虑线程安全:

没有操作共享变量的代码没有安全问题

对共享变量的读,使用volatile修饰变量即可

对共享变量的写,使用synchronized加锁

🍊二. 单例模式

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例

例如:DataSource(数据连接池),一个数据库只需要一个连接池对象

单例模式分为饿汉模式和懒汉模式

🌴1. 饿汉模式

饿汉模式是在类加载的时候就创建实例

这种方式是满足线程安全的(JVM内部使用了加锁,即多个线程调用静态方法,只有一个线程竞争到锁并且完成创建,只执行一次)

👁‍🗨️实现代码:


public class Singleton {
    //类加载阶段就会创建实例
    private static Singleton instance = new Singleton();
    //为了防止不小心new了这个SingletonEH,所构造方法访问权限为private
    private Singleton(){
    }
    //提供一个方法可以让外边能够拿到这个实例
    public static Singleton getInstance(){
        return instance;
    }
}

🌾2. 懒汉模式

懒汉模式是在类加载的时候不创建实例,第一次使用的时候才创建

👁‍🗨️实现代码:


public class Singleton {
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

观察上述代码,在单线程下不存在线程安全问题,但是在多线程环境下存在安全问题吗?

分析:

🍃当实例没有被创建的时候,如果有多个线程都调用getInstance方法,就可能创建多个实例,就存在线程安全问题

🍃但是实例一旦创建好,后面线程调用getInstance方法就不会出现线程安全问题

结果: 线程安全问题出现在首次创建实例的时候

🌵3. 懒汉模式(使用synchronized改进)

我们使用sychronized修饰,👁‍🗨️代码如下:

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){
    }
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

这样实现线程安全存在什么问题呢?

解析:

我们对方法使用synchronized修饰,也就是每次调用该方法的时候都会竞争锁,但是创建实例只需要创建一次,也就是创建实例后,再调用该方法还需要竞争锁释放锁

结果: 虽然满足线程安全,但是效率低

🌳4. 懒汉模式(使用双重校验锁改进)

在上述代码的基础上进行改动:

使用双重if判定,降低竞争锁频率

使用volatile修饰instance

👁‍🗨️实现代码:


public class Singleton {
    //加volatile的原因:禁止指令重排序
    private static volatile Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance(){
        if(instance == null){ //外层的if判断:如果实例被创建直接return,不让线程再继续竞争锁
            //在没有创建实例时,多个线程已经进入if判断了
            //一个线程竞争到锁,其他线程阻塞等待
            synchronized (Singleton.class) {
                //内层的if判断,目的是让竞争失败的锁如果再次竞争成功的话判断实例是否被创建,创建释放锁return,没有则创建
                if(instance == null){ 
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

🍬对双重if的解析:

🍂外层的if判断:实例只是被创建一次,当实例已经被创建好了就不要后续操作,直接return返回

🍂内层的if判断:实例未被创建时,多个线程同时竞争锁,只有一个线程竞争成功并创建实例,其他竞争失败的线程就会阻塞等待,当第一线程释放锁后,这些竞争失败的线程就会继续竞争,但是实例已经创建好了,所以需要再次进行if判断

画图分析,如下所示:

image.png

🌾5. 静态内部类

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存,懒汉式单例类线程安全控制烦琐,而且性能受影响

静态内部类实现单例模式就可以克服以上两种单例模式的缺点,如下所示👇

👁‍🗨️实现代码:


public class Singleton {
    private static class HolderClass{
        private static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return HolderClass.instance;
    }
}

由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

🌵6. 枚举

枚举是在JDK1.5以及以后版本中增加的一个“语法糖”,它主要用于维护一些实例对象固定的类。例如一年有四个季节,就可以将季节定义为一个枚举类型,然后在其中定义春、夏、秋、冬四个季节的枚举类型的实例对象。 按照Java语言的命名规范,通常,枚举的实例对象全部采用大写字母定义,这一点与Java里面的常量是相同的。

👁‍🗨️实现代码:


public enum Singleton {
     INSTANCE;
     public void businessMethod() {
          System.out.println("我是一个单例!");
     }
}

🥭三. volatile的原理

volatile保证了可见性,有序性,在Java层面看,volatile是无锁操作,多个线程对volatile修饰的变量进行读可以并发并行执行,和无锁执行效率差不多

volatile修饰的变量中,CPU使用了缓存一致性协议来保证读取的都是最新的主存数据

缓存一致性:如果有别的线程修改了volatile修饰的变量,就会把CPU缓存中的变量置为无效,要操作这个变量就要从主存中重新读取

🍏四. volatile的扩展问题(了解)

🍬如果说volatile不保证有序性,双重校验锁的写法是否有问题?

关于new对象按顺序分为3条指令:

🍁(1) 分配对象的内存空间

🍁(2) 实例化对象

🍁(3) 赋值给变量

正常的执行顺序为(1)(2)(3),JVM可能会优化进行重排序后的顺序为(1)(3)(2)

这个重排序的结果可能导致分配内存空间后,对象还没有实例化完成,就完成了赋值

在这个错误的赋值后,instance==null不成立,线程就会拿着未完成实例化的instance,使用它的属性和方法就会出错

使用volatile保证有序性后:

线程在new对象时不管(1)(2)(3)是什么顺序,后续线程拿到的instance是已经实例化完成的

CPU里边,基于volatile变量操作是有CPU级别的加锁机制(它保证(1)(2)(3)全部执行完,写回主存,再执行其他线程对该变量的操作)



相关文章
|
11天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
7月前
|
安全 Java
线程安全的单例模式(Singleton)
线程安全的单例模式(Singleton)
|
4月前
|
安全 Java 关系型数据库
单例模式下引发的线程安全问题
单例模式确保类在进程中仅有一个实例,适用于如数据库连接等场景。分为饿汉式与懒汉式:饿汉式在类加载时创建实例,简单但可能浪费资源;懒汉式延迟创建实例,需注意线程安全问题,常采用双重检查锁定(Double-Checked Locking)模式,并使用 `volatile` 关键字避免指令重排序导致的问题。
80 2
单例模式下引发的线程安全问题
|
3月前
|
设计模式 安全 Java
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
59 0
|
6月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
95 1
|
5月前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。
111 0
|
6月前
|
微服务
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
|
6月前
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
40 0
|
6月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
77 0
|
6月前
|
设计模式 安全 Java
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
82 0