【Java多线程案例】单例模式

news/2024/11/14 21:34:06/

本期讲解单例模式的饿汉模式与懒汉模式,以及如何解决懒汉模式造成线程的不安全问题。

目录

什么是单例模式?

1. 饿汉模式

2. 懒汉模式

2.1 懒汉模式单线程版

2.2 懒汉模式多线程版

3.  解决懒汉模式不安全问题

3.1 保证原子性

3.2 防止指令重排序


什么是单例模式?

首先,单例模式是一种设计模式。何为设计模式,设计模式类似于固定的套路。例如考驾照的科目二项目,教练会总结出一些点位,因此我们按照这些点位去练习然后考试就能很顺畅的通过。在 Java 中也是如此,常见的就是开发中前辈设计好的一些案例,我们直接拿来用即可。

单例模式是在进程中有且仅有一份实例的模式,所以我们称之为单例。此外单例模式分为饿汉模式懒汉模式

通过上图,我们可以看到。thread1 - thread3 都共用 Singleton 这个实例,这样的一个模式就是单例模式。


1. 饿汉模式

看到饿汉二字,我们就会想到这是一种饥渴的状态,有一种一看到饭就冲上去吃的感觉。因此,饿汉模式它是一种类加载时就创建对象的一种模式,如下代码:

//自定义类singleton
class Singleton {//创建一个对象private static Singleton instance = new Singleton();//提供一个获取instance的方法private static Singleton getInstance(){return instance;}
}

当以上代码中的自定义类 singleton 被加载后,就会创建一个 instance的对象。这时候我们就可以通过一个获取 instance 对象的方法 getInstance 来使用这个实例。由于 singleton 类中的所有成员变量与成员方法都是被 private 修饰,因此达到了封装效果也体现出了单例模式的唯一性

饿汉模式强调一个饥渴,类一被加载就创建了一个对象。它不存在线程安全问题,当多个线程调用这个饿汉模式时得到的都是同一个实例,并不重新创建实例。

以上的饿汉模式,设计得还是有问题的。如果我们新建了一个实例,这样就不能保证饿汉模式是一个单例模式,如以下代码:

public static void main(String[] args) {//s1和s2都是同一个实例Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();//s3新建了一个实例Singleton s3 = new Singleton();}

因此,我们必须保证在 Singleton 类不能被实例化,这时我们可以在 Singleton类 中提供一个被 private 修饰的构造方法,这样无论如何 Singleton 类都不能被 new 了。如下行代码:

private Singleton(){};

2. 懒汉模式

饿汉模式体现了一种饥渴,懒汉模式给人感觉就是一种懒散的状态,一碗饭在面前爱吃不吃的感觉。因此,懒汉模式在创建实例时并不在类加载时创建对象,而是什么时候需要创建对象了就去创建,不需要则不创建

举个例子,在家里面,吃午餐用了五个盘子,由于很懒没有及时的去洗。到了晚上,炒菜发现没盘子可用了才洗个盘子用来盛菜。剩余的四个盘子还是不洗,至于臭了还是烂了并不在意。这就是体现出懒汉模式中的“懒”状态。


2.1 懒汉模式单线程版

通过上方例子的讲解,我们可以了解到。懒汉模式在使用某个对象时,得判断该是否实例化。如果实例化过了就不创建直接返回该实例,没有则创建后返回该实例。如下流程图:

案例:通过懒汉模式创建一个自定义类 SingletonLazy ,并在 main 方法中创建两个 SingletonLazy 类的引用 s1、s2,使得 s1 等于 s2。因此,我们可以写出以下代码:

class SingletonLazy {//创建一个SingletonLazy的实例为空private static SingletonLazy instance = null;//获取该实例的方法public static SingletonLazy getInstance(){if (instance == null) {instance = new SingletonLazy();}return instance;}//提供一个private修饰的构造方法,保证唯一性private SingletonLazy(){};
}
public class ThreadDemo2 {public static void main(String[] args) {//s1和s2是同一个实例SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}

运行后打印:

以上代码,与饿汉模式相比来说更符合与现实开发。当然,上述代码是单线程版的饿汉模式。因此是比较安全的,但是把以上代码应用到多线程情况下就会造成线程不安全问题。


2.2 懒汉模式多线程版

首先,我们来看下上文中创建的懒汉模式的 SingletonLazy 类的代码。

class SingletonLazy {//创建一个SingletonLazy的实例为空private static SingletonLazy instance = null;//获取该实例的方法public static SingletonLazy getInstance(){if (instance == null) {instance = new SingletonLazy();}return instance;}//提供一个private修饰的构造方法,保证唯一性private SingletonLazy(){};
}

在多线程的学习中,我们以及知道了造成线程不安全有线程抢占资源这个概念,其原因就是多个线程在执行过程中进行读和写操作也就是修改操作,导致的不安全问题。

多线程下,懒汉模式会导致创建多个实例,因此不能保证实例的唯一性。假如有多个线程进行调用了 getInstance 方法。线程1在执行同时,由于运行速度过快,线程2也开始执行了,导致最后创建了两个实例。这样就不叫单例模式了。


3.  解决懒汉模式不安全问题


3.1 保证原子性

在上方创建的懒汉模式中的 if 语句 和 new 操作,是不具备原子性的。其原因为在多个线程调用 getIstance 这个方法。


上文中设计的懒汉模式预期的效果为:当多个线程调用 getInstance 方法后。第一个调用 getInstance 的线程会进行 new 操作创建一个 instance 实例,其他线程调用 getInstance 方法后发现 instance 不为 null 则不进行 new 操作。

但由于线程的抢占式执行,导致第一个调用 getIstance 的线程执行到第一步后,其他线程抢占执行了并调用了 getIstance 方法,这个时候两个线程 if 语句都判断 instance 等于 null 。这时候就创建了两个 instance 对象。

这样就导致 if 语句 和 new 操作就不具备原子性(不能完整的执行)。因此,我们可以使用 synchronized 关键字来加锁,使得这两个操作具备原子性。如下代码所示:

//获取该实例的方法public static SingletonLazy getInstance(){//给if语句和new操作加锁synchronized (Singleton.class) {if (instance == null) {instance = new SingletonLazy();}}return instance;}

当然,以上代码加上了锁虽然保证了 if 语句和 new 操作具备了原子性,但还不算是最优的写法。我们可以想象一下,每个线程调用 getInstance 这个方法时候,都会进行锁的竞争这样就会阻塞等待,这样的时间效率是非常低的。

因此,我们可以使用双重 if 语句来减少阻塞等待。如下代码:

public static SingletonLazy getInstance(){if (instance == null) {//给if语句和new操作加锁synchronized (Singleton.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}

以上的代码中两条 if 语句里面的条件是一样的,但其初心不同。第一条 if 是为了判断是否有 instance 这个实例不让其他线程进入锁的竞争,第二条 if 语句是在锁下进行判断的创建唯一一个实例。

当然,可能多个线程抢占并进入到了第一条 if 语句,但第一个进入锁的线程完成了创建实例任务后,其他线程进入锁后 if 判断的实例不为空也就不会再多创建实例了。

这样的设计才是懒汉模式的标准写法,保证了实例的唯一性。但有一极端的情况,指令被重排序了,具体请看下方讲解。


3.2 防止指令重排序

有一种极端的情况,两个线程同时调用了 getInstance 方法,都进入了第一条 if 语句里面。线程1进入了锁(synchronized)的范围,但由于指令重排序导致 new 这个操作与原本执行顺序不一致。这时候,线程2进入了锁的范围,发现 instance 实例已被创建,则返回 instance 实例。

这样就会导致线程2调用的构造方法是虚无的、不知道是哪里的,造成了线程的不安全。因此,我们在初始化 instance 实例时加上 volatile 关键字,使得指令能够按照顺序进行。

volatile private static SingletonLazy instance = null;

综合起来,创建一个懒汉模式的代码如下所示:

class SingletonLazy {//创建一个SingletonLazy的实例为空,volatile修饰保证指令顺序执行volatile private static SingletonLazy instance = null;//获取该实例的方法public static SingletonLazy getInstance(){//判断instance实例是否存在,存在则返回if (instance == null) {//给if语句和new操作加锁,防止多new操作synchronized (Singleton.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}//提供一个private修饰的构造方法,保证唯一性private SingletonLazy(){};
}

总结:

  • 单例模式是在进程中有且仅有一份实例的模式。
  • 单例模式分为饿汉模式与懒汉模式。
  • 饿汉模式天然不存在线程不安全问题。
  • 懒汉模式存在线程不安全问题,因此需要进行加锁(synchronized)操作与防止指令重排序(volatile)操作。

🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。

🗃️文章收录于:Java多线程编程

🗂️JavaSE的学习:JavaSE

🗂️Java数据结构:数据结构与算法

 本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~


http://www.ppmy.cn/news/78358.html

相关文章

opencv_04条形码区域分割

基于OpenCV的条形码区域分割 要基于OpenCV实现条形码区域分割,可以按照以下步骤进行: 加载图像:使用OpenCV中的imread函数读取待处理图像。灰度化:使用OpenCV中的cvtColor函数将彩色图像转换为灰度图像。边缘检测:使用…

Thinkphp漏洞详解合集

文章目录 Thinkphp6.0.12LTS反序列化漏洞环境漏洞分析 thinkphp lang命令执行环境影响版本漏洞分析漏洞复现 CNVD-2018-24942(t5RCE)环境影响版本漏洞分析漏洞复现 Thinkphp3.2.x命令执行环境漏洞分析漏洞复现 thinkphp-2x-rce漏洞环境影响版本漏洞分析 Thinkphp5.0.23变量覆盖…

【机器学习】线性回归模型详解

PS:本文有一定阅读门槛,如果有不明白的地方欢迎评论询问! 1 模型概述 接下来我们将要学习我们的第一个模型——线性回归。比如说我需要根据数据预测某个面积的房子可以卖多少钱 接下来我们会用到以下符号: m:训练样本数量x:输…

Flutter控件之Tab选项卡封装

Tab选项卡,这是一个非常常见且权重很高的一个组件,随便打开一个App,比如CSDN,如下图,首页顶部就是一个Tab选项卡,这个功能可以说,几乎每个App都会存在。 在Android中,我们可以使用Ta…

EduSoho 网校系统部署

目录 一、初始化环境二、安装 Nginx三、安装 MySQL四、安装 PHP五、上线 EduSoho六、验证FAQ 一、初始化环境 1、安装wget 如果系统已安装wget,请跳过此步骤 yum install wget2、关闭防火墙/Selinux systemctl stop firewalld.service systemctl disable firewalld.service…

华为ACL(基本的高级的基于时间的自反的都有)

第12章:ACL 随着网络的飞速发展,网络安全和网络服务质量QoS (Quality of Service)问题日益突出。访问控制列表 (ACL, Access Control List)是与其紧密相关的一个技术。ACL可以通过对网络中报文流的精确识别,与其他技术结合,达到控制网络访问行为、防止网络攻击和提高网络带…

助力工业物联网,工业大数据之ODS层构建:申明分区代码及测试【十】

文章目录 知识点13:ODS层构建:申明分区代码及测试知识点14:ODS层与DWD层区别知识点15:DWD层构建:需求分析知识点16:DWD层构建:建库实现测试知识点17:DWD层构建:建表实现测…

【2023】某python语言程序设计跟学第八周内容

目录 1.实例:体育竞技分析1.1.问题分析1.2.自顶向下和自底向上1.3.体育竞技分析 2.python程序设计思维2.1.计算思维与程序设计2.2.计算生态与python语言2.3.用户体验与软件产品2.4.基本的程序设计模式 3.Python第三方库安装3.1.看见更大的Python世界3.2.pip安装方式…