多线程案例-单例模式

简介: 多线程案例-单例模式

单例模式

设计模式的概念

设计模式好比象棋中的"棋谱".红方当头炮,黑方马来跳.针对红方的一些走法,黑方应招的时候有一些固定的套路.按照套路来走局势就不会吃亏.

软件开发中也有很多常见的"问题场景".针对这些问题的场景,大佬们总结出了一些固定的套路.按照这些套路来实现代码,也不会吃亏

单例模式概念

单例 = 单个实例(对象)

具体来说,就是某个类,在一个进程中,只应该创建出一个实例.(也就是原则上不应该有多个)

使用单例模式,就可对代码进行更严格的校验与检查.

期望让机器(编译器)能够对代码中指定的类,创建的实例个数,进行校验.如果发现创建多个实例了,就直接让编译器报错这种~~

这一点在很多场景上都需要,一般就是一个对象持有(管理)大量数据时,比如JDBC中的DataSource实例只需要一个.

单例模式具体的实现方式有很多.最常见的是"饿汉"和"懒汉"两种.

饿汉模式

类加载的同时,创建实例.

也就是说实例在类加载的时候就创建了,创建时机非常早,相当于程序一启动,实例就创建了.

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return instance;
    }
}
 
public class TestSingleton {
    public static void main(String[] args) {
        Singleton.getInstance();
 
        Singleton s = new Singleton();
    }
}

1.instance是Singleton类对象里持有的属性.类对象是指Singleton.class(就是从.class加载至内存中,表示类的一个数据结构).

2.private Singleton() {} 是在设置私有构造方法,保证其它代码不能创建出新的对象.

比如:Singleton s = new Singleton();在这里就无法执行

3.其它代码如果想要获得这个类的唯一实例,就可以通过getInstance()方法获取.

对于饿汉来说,getInstance直接返回Instance实例,这个操作本质上是"读操作",多个线程读取同一个变量,是线程安全的.

懒汉模式-单线程版

类加载的时候不创建实例.第一次使用的时候才创建实例.

class Singleton {
    private static Singleton instance = null;
    //这个引用先初始化为null,而不是立即创建实例.
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在这个代码中,首次调用getInstance时,instance引用为null.进入里面的if条件,把实例创建出来.如果后续再次调用,if就不进入.而是直接返回之前创建的引用了.

这样设定,仍可以保证该类的实例是唯一一个.于此同时,创建实例的时机就不是程序驱动的了,而是第一次调用getInstance时(操作执行时机看程序具体需求.大概率要比饿汉这种方式要晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作给省下了).

注意:懒汉模式是比饿汉模式更好一些的.

在计算机中,懒的思想非常有意义:

比如有一个非常大的文件(10GB).有一个编辑器,使用编辑器打开这个文件.

如果是按照"饿汉模式",编辑器就会先把这10GB的数据加载到内存中,然后再进行统一的展示.(即使加载了这么多数据,用户还得一点一点看,没法一下子看完这么多..)

如果是按照"懒汉模式",编辑器就会只读取一小部分数据(比如只读10KB),把这10KB先展示出来.随着用户进行翻页之类的操作,再继续读后续的数据.

懒汉模式-多线程版

上面的懒汉模式是线程不安全的.

线程安全发生在首次创建实例时.如果在多个线程中同时调用getInstance方法,就可能导致创建出多个实例.

一旦实例已经创建好了,后面再多线程环境调用getInstance就不再有线程安全问题了(不再修改Instance了).

举个例子:

譬如这种情况,两次的if条件都符合,会创建两个实例,显然不符合规定.

而这时就很容易想到使用synchronized来解决这个问题.

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

这样写确实可以解决线程安全的问题.但还是有一个问题:

比如Instance已经创建过了.此时后续再调用getInstance就都是返回Instance实例了吧(于是此处的操作就是纯粹的读操作了,也就不会有线程安全问题了).

此时,针对这个已经没有线程安全问题的代码,仍然时每次调用都先加锁再解锁,此时效率就非常低了!!!(加锁意味着会产生阻塞,一旦线程阻塞,啥时候能解除,就不知道了.你可以认为:只要一个代码里加锁了,基本注定就要和"高性能"无缘).

因此我们说,在不该加锁的时候是不能乱加的.

解决方案:可以在加锁外面再套一层if,以判断是否加锁.(如果instance为null,说明是首次调用,首次调用就需要考虑线程安全问题->要加锁 / 如果非null,说明是后续调用->不必加锁)

再来看一下修改的代码:

class Singleton {
    private static Object locker = new Object();
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {//第一个if判定的是是否加锁(保证执行效率)
            synchronized(locker) {
                if(instance == null) {//第二个if判定的是是否要创建对象(保障线程安全)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
 

但是又双有一个问题,就是指令重排序引起的线程安全问题.

我们知道,指令重排序,也是编译器优化的一种方式.(调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率).

这里指的就是instance = new Singleton();

这条语句可以拆分成多个指令:(1)申请一段内存空间 (2)在内存上调用构造方法,创建出这个实例 (3)把这个内存地址赋给Instance引用变量

正常情况下:是按照(1)(2)(3)顺序执行的,但编译器也可优化成(1)(3)(2)执行,多线程指令重排序可能有问题.

原因如下:

解决方案:给instance加上volatile(volatile可以防止指令重排序).

加上之后,针对这个变量的读写操作,就不会出现指令重排序了.

最后代码如下:

 

class Singleton {
    private static Object locker = new Object();
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {//第一个if判定的是是否加锁(保证执行效率)
            synchronized(locker) {
                if(instance == null) {//第二个if判定的是是否要创建对象(保障线程安全)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 

相关文章
|
1天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
25 1
|
3月前
|
安全 Java 关系型数据库
单例模式下引发的线程安全问题
单例模式确保类在进程中仅有一个实例,适用于如数据库连接等场景。分为饿汉式与懒汉式:饿汉式在类加载时创建实例,简单但可能浪费资源;懒汉式延迟创建实例,需注意线程安全问题,常采用双重检查锁定(Double-Checked Locking)模式,并使用 `volatile` 关键字避免指令重排序导致的问题。
70 2
单例模式下引发的线程安全问题
|
2月前
|
设计模式 安全 Java
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
57 0
|
3月前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
69 0
|
4月前
|
消息中间件 安全 Kafka
"深入实践Kafka多线程Consumer:案例分析、实现方式、优缺点及高效数据处理策略"
【8月更文挑战第10天】Apache Kafka是一款高性能的分布式流处理平台,以高吞吐量和可扩展性著称。为提升数据处理效率,常采用多线程消费Kafka数据。本文通过电商订单系统的案例,探讨了多线程Consumer的实现方法及其利弊,并提供示例代码。案例展示了如何通过并行处理加快订单数据的处理速度,确保数据正确性和顺序性的同时最大化资源利用。多线程Consumer有两种主要模式:每线程一个实例和单实例多worker线程。前者简单易行但资源消耗较大;后者虽能解耦消息获取与处理,却增加了系统复杂度。通过合理设计,多线程Consumer能够有效支持高并发数据处理需求。
199 4
|
5月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
89 1
|
4月前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。
103 0
|
5月前
|
微服务
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
多线程内存模型问题之在单例模式中,volatile关键字的作用是什么
|
5月前
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
38 0