当前位置: 首页 > news >正文

多线程编程的简单案例——单例模式[多线程编程篇(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个字

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

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

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

相关文章:

  • NFC 碰一碰发视频源码搭建,碰一碰发视频定制化开发技术
  • Redis 的指令执行方式:Pipeline、事务与 Lua 脚本的对比
  • ROS机器人一般用哪些传感器?
  • 初识Redis · 客户端“Hello world“
  • R 语言科研绘图 --- 饼状图-汇总
  • Yum镜像源
  • 中间件--ClickHouse-10--海量数据存储如何抉择ClickHouse和ES?
  • 【系统分析师】-软件工程
  • 【文件操作与IO】详细解析文件操作与IO (一)
  • 探索 Higress:下一代云原生 API 网关
  • 前端融合图片mask
  • 高级java每日一道面试题-2025年4月13日-微服务篇[Nacos篇]-Nacos如何处理网络分区情况下的服务可用性问题?
  • ubantu18.04(Hadoop3.1.3)之MapReduce编程
  • pnpm解决幽灵依赖问题
  • Model Context Protocol (MCP) 开放协议对医疗多模态数据整合的分析路径【附代码】
  • Kaamel隐私与安全分析报告:Microsoft Recall功能评估与风险控制
  • hadoop和Yarn的基本介绍
  • 使用Java动态数据生成PDF报告:简化您的报告导出流程
  • AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容
  • kafka菜鸟教程
  • 韩国检方起诉前总统文在寅
  • 央媒关注脑瘫女骑手:7年跑出7.3万多份单,努力撑起生活
  • 聚焦“共赢蓝色未来”,首届 “海洋命运共同体”上海论坛举行
  • 研究显示:日行9000步最高可将癌症风险降低26%
  • 北朝时期的甲胄
  • 南北皆宜的“中国酒都”宿迁:下一程如何更“醇厚绵长”