目录
前言
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()方法:让指定的程序进入阻塞状态
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个字
本来笔者想接着介绍阻塞队列的,看来只能留到下次了!
笔者的风格是每一步都会写的很详细,因为笔者觉得自己天赋不佳,需要在学会的时候记录的越详细越好,方便读者查阅和调用
希望笔者如此之高质量的博客能帮助到你我他!