多线程编程的简单案例——单例模式[多线程编程篇(3)]

目录

前言

1.wati() 和 notify()

wait() 和 notify() 的产生原因

如何使用wait()和notify()?

 案例一:单例模式 

 饿汉式写法:

 懒汉式写法 

对于它的优化

 再次优化

结尾 

前言

如何简单的去使用jconsloe 查看线程 (多线程编程篇1)_eclipse查看线程-CSDN博客

浅谈Thread类及常见方法与线程的状态(多线程编程篇2)_thread.join() 和thread.get()-CSDN博客

这是系列的第三篇博客,这篇博客笔者想结合自己的学习经历,分享几个多线程编程的简单案例,帮助读者们更快的理解多线程编程,也非常感激能耐心阅读本系列博客的读者们!

本篇博客的内容如下,您可以通过目录导航直接传送过去

1.介绍wait()和notify()这两个方法

2.介绍单例模式

废话不多说,让我们开始吧,希望我们在知识的道路上越走越远!

博客中出现的参考图都是笔者手画或者截图的的

代码示例也是笔者手敲的!

影响虽小,但请勿抄袭

1.wati() 和 notify()

wait() 和 notify() 的产生原因

在多线程编程中,多个线程同时读写共享资源非常常见。假设两个线程要交替操作一个数据,比如:

  • 线程A:负责生产数据;

  • 线程B:负责消费数据。

如果没有协调机制,线程A和线程B的执行顺序完全由CPU调度,极有可能出现这种情况:

  • 线程B执行时,发现A还没生产好;

  • 线程A刚生产好,B却还没来消费。

这样会出现资源使用错误,甚至死循环。

所以,Java提供了 wait()notify(),解决线程之间通信的问题,帮助程序做到:

 一个线程在条件不满足时,自动等待。
 另一个线程操作完后,主动唤醒等待的线程。

这种机制,叫做等待-通知机制"。

具体来说:

wait()方法:让指定的程序进入阻塞状态

wait 结束等待的条件 :
1.其他线程调用该对象的 notify 方法 .
2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 ).
3.其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 .

notify()方法:唤醒对应的处在阻塞状态的线程.

举个生活中的例子:

假设你去银行取号排队:

  • 你取号后坐在椅子上等待(相当于调用 wait() 进入等待状态)。

  • 银行的叫号系统喊你的号码时,你再去窗口办理业务(相当于 notify() 唤醒你)。

如果没有这个等待机制,你可能得不停地站在窗口问“轮到我了吗?什么时候才能到我啊?前面的人能不能tm快点啊!”(浪费CPU资源)

有了 wait()notify(),就能让线程“高效地等待”而不是死循环轮询

如何使用wait()和notify()?

OK了解了他们的概念和作用,接下来,笔者将介绍如何使用wait()和notify()

首先,读者们需要了解一些前置知识

第一:根据源码文档,wait() 方法在调用时,必须处理 InterruptedException
因此使用时要么用 try-catch 捕获,要么在方法上声明 throws,否则代码无法通过编译。

第二:wait() 和 notify() 方法并不是定义在 Thread 类中,而是属于 Object 类的方法。
所以在实际使用中,我们通常需要先创建一个 Object 对象,通过这个对象来调用 wait()和 notify(),并且配合 synchronized 关键字一起使用,确保线程安全。

请看一组示例代码:

public class Demo
{public static void main(String[] args) {Object  ob = new Object();Object  lock = new Object();Thread thread1 = new Thread(() ->{synchronized (ob){System.out.println("wait 之前");try {ob.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("进入了");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait 之后");});
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒, 重新尝试获取这个锁.Thread thread2 = new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (ob){System.out.println("通知了");ob.notify();}});thread1.start();thread2.start();}
}

在使用 wait()notify() 这两个方法时,有一个非常重要的前提条件:

调用它们时,必须先持有调用对像的锁,而且必须时同一个对像,否则会抛出异常

我们一定要保证,哪个对像调用了wati(),哪个对像就要调用notify(),或者也要设置好阻塞时间. 

synchronized (ob) {ob.wait();  //  正确,线程1的锁对象是 ob
}synchronized (lock) {ob.notify();  //  错误,线程2的锁对象是 lock,调用 notify 却针对 ob
}
错误写法
synchronized (ob) {ob.wait();  //  正确,线程1的锁对象是 ob
}synchronized (ob) {ob.notify();  
正确写法


 案例一:单例模式 

 单例模式是一种设计模式

它保证了一个类在内存中永远只会有一个对象实例.并且提供全局访问点。

举个例子:

假设你要开发一个系统中的配置文件读取器,配置文件只需要加载一次,所有模块都要读取相同的配置信息。如果每次调用都重新 new 一个对象,不仅浪费内存,而且可能导致配置不一致。
通过单例模式,你可以保证这个读取器在整个程序运行期间只创建一次,并且全局唯一! 

又或者 比如 JDBC 中的 DataSource 实例就只需要一个!!!

 单例模式也有两种写法 : 

1.懒汉式: 只要在需要被实例化的时候,才会被实例化.

2.饿汉式:顾名思义,在类内部创建唯一实例,并且用 private static final 修饰,保证类一旦被加载了,就开始实例化了

 饿汉式写法:

public class Singleton {// 饿汉单例,类一旦被加载,就开始实例化了// 1️⃣ 在类内部创建唯一实例,并用 `private static final` 修饰private static final Singleton demo = new Singleton();// 2️⃣ 私有构造方法,防止外部创建实例// 静态代码块private Singleton() {System.out.println("Singleton 实例被创建");}// 3️⃣ 提供公共方法获取实例public static Singleton getInstance() {return demo;}
}

在饿汉式单例中,我们会直接在类内部创建好对象实例,当类加载进内存时,实例就已经完成了初始化。

这是因为我们使用了 static 关键字来修饰这个实例,static 属于类本身,随着类的加载而初始化。
所以,只要 JVM 加载这个类,单例对象就会被创建,并且保证全局只有一个。

在 Java 中,static 修饰的属性或方法属于类本身,而不是某个具体对象。
类被加载到内存时,所有 static 修饰的成员(属性、方法、代码块)会随类一起初始化,而且只会初始化一次。

也就是说:

  • 类加载时,static 属性会被分配内存并初始化。

  • static 方法属于类本身,不依赖对象,可以通过类名.方法名()调用。

我们简单测试一下:

class  MyTest
{public static void main(String[] args) {Singleton s1 =  Singleton.getInstance();}
}

调用  Singleton.getInstance()的时候,类被加载,demo被初始化,并且  Singleton() 构造方法被执行,打印"Singleton 实例被创建".

 
懒汉式写法 

类加载的时候不创建实例 . 第一次使用的时候才创建实例 . 我们依据这个思路,写出来懒汉式单例
public class SingletonLazy {// 1️⃣ 声明一个静态变量用来存储实例private static  SingletonLazy instance;// 2️⃣ 私有构造方法,防止外部创建实例private SingletonLazy() {System.out.println("SingletonLazy 构造方法执行:对象创建成功!");}// 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化public static SingletonLazy getInstance() {instance = new SingletonLazy();return instance;}

为了测试懒汉和饿汉的不同,我们再写两个辅助的静态方法测试:

public class SingletonLazy {// 1️⃣ 声明一个静态变量用来存储实例private static  SingletonLazy instance;// 2️⃣ 私有构造方法,防止外部创建实例private SingletonLazy() {System.out.println("SingletonLazy 构造方法执行:对象创建成功!");}// 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化public static SingletonLazy getInstance() {instance = new SingletonLazy();return instance;}static {System.out.println("SingletonLazy 类已加载!");}public static void printf() {System.out.println("调用了静态方法 printf()");}}

测试一下:

class Test {public static void main(String[] args) {// 不调用 getInstance 只调用静态方法SingletonLazy.printf();  // 会触发类加载,但不会创建对象!System.out.println("---------------");// 真正调用 getInstance,才会创建对象SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();}
}

结果如下:

调用静态方法后,类会被加载,但此时并不会执行构造方法,也就是说对象还没有被创建。只有当调用 getInstance()  方法时,程序才会真正实例化对象,执行构造方法,完成对象的创建!

我们还可以做一点优化,我们都知道这是单例模式, 只允许有一个对象实例,那么,只有第一次访问时才需要被创建,后续就不用再次创建了,因此可以写成:

public class SingletonLazy {// 1️⃣ 声明一个静态变量用来存储实例private static volatile SingletonLazy instance;// 2️⃣ 私有构造方法,防止外部创建实例private SingletonLazy() {System.out.println("SingletonLazy 构造方法执行:对象创建成功!");}// 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();           }return instance;}
}

 如果在单线程编程下,这样就挑不出毛病了!

对于它的优化

但是,假设在多线程环境下,有复数个线程同时调用  getInstance() ,那么就会创建出多个实例

举一个具体的例子

一旦程序进入多线程环境,比如存在A、B、C 三个线程,它们几乎在同一时刻调用 getInstance()方法

在这一瞬间,instance 的确是 null,三个线程会同时通过 if 判断,然后同时执行 new SingletonLazy(),最终结果就是:

创建了多个实例,破坏了单例模式!!!

因此,我们希望判断是否为空,以及创建实例,这两个动作"原子化"——即不会也不能被打断

怎么办?聪明的你肯定想到了,加锁!

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

加完锁以后,刚刚的情况就会变为:

1.假设程序运行在多线程环境下,A、B、C 三个线程几乎在同一时间,调用了 getInstance() 方法。

2.在这一瞬间,instance 的确是 null,于是三个人一起冲进来,准备创建对象。但是!因为这里加了 synchronized,所以三个线程必须抢锁,只有一个幸运儿能抢到,比如A线程。

3.然后A线程释放锁,B、C线程后面排队进来,发现 instance 已经不再是 null,所以它们就啥也不干,直接返回已有的实例。

4.这样一来,就保证了全局唯一实例,不会被多线程同时创建多个,单例模式真正实现了!

 再次优化

不过啊,虽然上面这种“方法加锁”确实解决了多线程下的安全问题——只要一个线程进来了,其他线程就乖乖排队,等着用同一个实例,表面上看没毛病。

但是!问题又来了:

每次调用 getInstance(),都要加锁。
不管 instance 有没有被创建,线程都得卡着 synchronized 排队。

想一想——如果我已经拿到实例了,后面无数次调用其实都只是想用一下这个对象,根本不需要再创建,可还是得老老实实抢锁,这效率能不低吗? 毕竟,加锁的开销也不小了.

所以,聪明的程序员又想了个办法,叫:

双重检查锁(Double-Check Locking),简称 DCL。

核心思路就一句话:

先检查,不满足再加锁,锁住后再检查,确认安全后再创建。

也就是说,外面先检查一次,里面再检查一次,这样只有在 instance 真正等于 null 的时候,才会走到创建对象的逻辑,其他时候,直接跳过锁,快速返回。

public class SingletonLazy {// 加上 volatile,防止指令重排序private static volatile SingletonLazy instance;private SingletonLazy() {System.out.println("SingletonLazy 构造方法执行:对象创建成功!");}public static SingletonLazy getInstance() {if (instance == null) {  // 第一次检查synchronized (SingletonLazy.class) {if (instance == null) {  // 第二次检查instance = new SingletonLazy();}}}return instance;}
}

而且还有个小细节,volatile 关键字也别忘了加上!

因为 Java 内存模型中,new 操作可能会被“重排序”

那么,还是刚刚ABC三线程竞争的例子:

1.

A、B、C 三个线程同时调用 getInstance(),一起执行第一次 if (instance == null)

2. 假设 instance 真的为 null,于是三个线程都准备往下走。

3.

A、B、C 到达 synchronized 这里,开始抢锁。假设A赢了,进入同步代码块。

A 再次执行第二次 if (instance == null),发现确实为空,于是创建 new SingletonLazy()
A 创建完成后,释放锁。

4.

B、C 排队进来,再次检查 if (instance == null),发现已经不为空了,直接跳过创建,返回已存在的实例。 

这样对比普通加锁的好处是,实例化以后,先判断一下是否是空,而不是多个线程直接去竞争锁导致资源浪费

总结一句话:
DCL的好处就是,实例化之后,线程们先看一眼:
"对象在不在?"
在,就立刻用!
不在,才排队抢锁。

相比“每次都抢锁”的方式,DCL大幅减少了资源浪费,尤其适合多线程访问频繁的场景。

完整代码:

public class SingletonLazy {// 1️⃣ 声明一个静态变量用来存储实例private static volatile SingletonLazy instance;// 2️⃣ 私有构造方法,防止外部创建实例private SingletonLazy() {System.out.println("SingletonLazy 构造方法执行:对象创建成功!");}// 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化public static SingletonLazy getInstance() {if(instance == null){synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}
// 外层 if 的作用:
// 避免已经实例化对象的情况下,仍然加锁。因为加锁是一种消耗性能的操作,
// 所以外层先判断,能直接返回就直接返回,提高效率。// 内层 if 的作用:
// 防止多个线程在 instance == null 的情况下,同时进入同步代码块,
// 抢锁后,重复创建实例。内层 if 可以保证只有第一个抢到锁的线程会创建实例。// 假设 instance 初始为 null,两个线程 A 和 B 几乎同时调用 getInstance():
// 【第一阶段:外层 if 判断(无锁)】
// - 线程A发现 instance == null,进入同步块等待抢锁。
// - 线程B也发现 instance == null,也准备进入同步块等待抢锁。// 【第二阶段:尝试获取锁】
// - 线程A抢到 synchronized(SingletonLazy.class) 的锁,进入同步块,开始执行内层代码。
// - 线程B未抢到锁,必须等待线程A释放锁,挂起等待。// 【第三阶段:内层 if 判断】
// - 线程A在内层再次检查 instance 是否为 null,
//   如果确实是 null,就创建 SingletonLazy 实例。
// - 线程A释放锁,线程B接着抢到锁。// 【第四阶段:线程B再次检查】
// - 线程B进入同步块,内层 if 判断时,发现 instance 已经不是 null,
//   所以不会再创建新对象,直接返回已存在的实例。// 【总结】
// 这样写的双重检查机制,既保证了线程安全,
// 又避免每次都去加锁,提升了性能!// 辅助方法,观察类是否加载static {System.out.println("SingletonLazy 类已加载!");}public static void printf() {System.out.println("调用了静态方法 printf()");}
}class Test {public static void main(String[] args) {// 不调用 getInstance 只调用静态方法SingletonLazy.printf();  // 会触发类加载,但不会创建对象!System.out.println("---------------");// 真正调用 getInstance,才会创建对象SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();}
}

结尾 

写到这里的时候,大约花费了笔者120分钟,写了8145个字

本来笔者想接着介绍阻塞队列的,看来只能留到下次了!

笔者的风格是每一步都会写的很详细,因为笔者觉得自己天赋不佳,需要在学会的时候记录的越详细越好,方便读者查阅和调用

希望笔者如此之高质量的博客能帮助到你我他!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/79430.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pytorch基本操作2

torch.clamp 主要用于对张量中的元素进行截断(clamping),将其限制在一个指定的区间范围内。 函数定义 torch.clamp(input, minNone, maxNone) → Tensor 参数说明 input 类型:Tensor 需要进行截断操作的输入张…

一次制作参考网杂志的阅读书源的实操经验总结(附书源)

文章目录 一、背景介绍二、书源文件三、详解制作书源(一)打开Web服务(二)参考网结构解释(三)阅读书源 基础(四)阅读书源 发现(五)阅读书源 详细(六…

并发设计模式实战系列(2):领导者/追随者模式

🌟 ​大家好,我是摘星!​ 🌟 今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~ 目录 领导者/追随者(Leader/…

自求导实现线性回归与PyTorch张量详解

目录 前言一、自求导的方法实现线性回归1.1自求导的方法实现线性回归的理论讲解1.1.1 线性回归是什么?1.1.2线性回归方程是什么?1.1.3散点输入1.2参数初始化1.2.1 参数与超参数1.2.1.1 参数定义1.2.1.2 参数内容1.2.1.3 超参数定义1.2.1.4 超参数内容1.…

2025年机电一体化、机器人与人工智能国际学术会议(MRAI 2025)

重要信息 时间:2025年4月25日-27日 地点:中国济南 官网:http://www.icmrai.org 征稿主题 机电一体化机器人人工智能 传感器和执行器 3D打印技术 智能控制 运动控制 光电系统 光机电一体化 类人机器人 人机界面 先进的运动控制 集成制造系…

线性代数 | 知识点整理 Ref 3

注:本文为 “线性代数 | 知识点整理” 相关文章合辑。 因 csdn 篇幅合并超限分篇连载,本篇为 Ref 3。 略作重排,未整理去重。 图片清晰度限于引文原状。 如有内容异常,请看原文。 《线性代数》总复习要点、公式、重要结论与重点释…

CFD中的动量方程非守恒形式详解

在计算流体力学(CFD)中,动量方程可以写成守恒形式和非守恒形式,两者在数学上等价,但推导方式和应用场景不同。以下是对非守恒形式的详细解释: 1. 动量方程的守恒形式 首先回顾守恒形式的动量方程&#xff…

Leetcode 1504. 统计全 1 子矩形

1.题目基本信息 1.1.题目描述 给你一个 m x n 的二进制矩阵 mat ,请你返回有多少个 子矩形 的元素全部都是 1 。 1.2.题目地址 https://leetcode.cn/problems/count-submatrices-with-all-ones/description/ 2.解题方法 2.1.解题思路 单调栈 时间复杂度&…

【Docker】运行错误提示 unknown shorthand flag: ‘d‘ in -d ----详细解决方法

使用docker拉取Dify的时候遇到错误 错误提示 unknown shorthand flag: d in -dUsage: docker [OPTIONS] COMMAND [ARG...]错误原因解析 出现 unknown shorthand flag: d in -d 的根本原因是 Docker 命令格式与当前版本不兼容,具体分为以下两种情况: 新…

华为OD机试真题——攀登者2(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 A卷 200分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析; 并提供Java、python、JavaScript、C、C语言、GO六种语言的最佳实现方式! 2025华为OD真题目录全流程解析/备考攻略/经验分享 华为OD机试真题《攀登者2…

qt硬件与软件通信中 16进制与十进制转化

1. 首先上代码, 这是在qt语言上的操作 截取 01 03 0C 00 00 00 00 00 00 00 0C 00 0C 00 0C 93 70 这串16进制数值进行处理,截取这样一段内容 00 0C 00 0C 00 0C 字节数组转字符串。从bytearray数组转换为string. QString CustomTcpSocket::recieveInfo() {QByteArr…

图形变换算法

一、学习目的 (1)掌握多面体的存储方法。 (2)掌握图形的几何变换及投影变换。 (3)掌握三维形体不同投影方法的投影图的生成原理。 (4)掌握多面体投影图绘制的编程方法。 二、学…

【JAVAFX】自定义FXML 文件存放的位置以及使用

情况 1:FXML 文件与调用类在同一个包中(推荐) 假设类 MainApp 的包是 com.example,且 FXML 文件放在 resources/com/example 下: 项目根目录 ├── src │ └── sample │ └── Main.java ├── src/s…

Ubuntu20.04安装企业微信

建议先去企业微信官网看一下有没有linux版本,没有的话在按如下方式安装,不过现在是没有的。 方案 1、使用docker容器 2、使用deepin-wine 3、使用星火应用商店 4. 使用星火包deepin-wine 5、使用ukylin-wine 本人对docker不太熟悉,现…

CSS appearance 属性:掌握UI元素的原生外观

在现代网页设计中,为了达到一致的用户体验,我们有时需要让HTML元素模仿操作系统的默认控件样式。CSS中的appearance属性提供了一种简便的方式来控制这些元素是否以及如何显示其默认外观。本文将详细介绍appearance属性,并通过实际代码示例来展…

十四、C++速通秘籍—函数式编程

目录 上一章节: 一、引言 一、函数式编程基础 三、Lambda 表达式 作用: Lambda 表达式捕获值的方式: 注意: 四、函数对象 函数对象与普通函数对比: 五、函数适配器 1、适配普通函数 2、适配 Lambda 表达式 …

大模型Rag-指令调度

本文主要记录根据用户问题指令,基于大模型做Rag,匹配最相关描述集进行指令调度,可用于匹配后端接口以及展示答案及图表等。 1.指令查询处理逻辑 1.实现思路 指令识别:主要根据用户的问题q计算与指令描述集is [i0, ... , im]和指…

音视频学习 - ffmpeg 编译与调试

编译 环境 macOS Ventrua 13.4 ffmpeg 7.7.1 Visual Studio Code Version: 1.99.0 (Universal) 操作 FFmpeg 下载源码 $ cd ffmpeg-x.y.z $ ./configure nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.If you think configure made a mistake…

golang-常见的语法错误

https://juejin.cn/post/6923477800041054221 看这篇文章 Golang 基础面试高频题详细解析【第一版】来啦~ 大叔说码 for-range的坑 func main() { slice : []int{0, 1, 2, 3} m : make(map[int]*int) for key, val : range slice {m[key] &val }for k, v : …

音视频之H.265/HEVC预测编码

H.265/HEVC系列文章: 1、音视频之H.265/HEVC编码框架及编码视频格式 2、音视频之H.265码流分析及解析 3、音视频之H.265/HEVC预测编码 预测编码是视频编码中的核心技术之一。对于视频信号来说,一幅图像内邻近像素之间有着较强的空间相关性,相邻图像之…