做网站一年百度应用app
文章目录
- 一、线程间的通信
- (1)为什么要处理线程间的通信
- (2)等待唤醒机制
- 二、案例
- (1)案例
- 1、创建线程
- 2、解决线程安全问题
- 3、等待
- 4、唤醒
- 5、同步监视器
- (2)调用wait和notify需注意的细节
- 三、wait与sleep的区别
一、线程间的通信
(1)为什么要处理线程间的通信
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行
,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。(在同步的基础之上解决通信的问题)
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即—— 等待唤醒机制。
(2)等待唤醒机制
这是多个线程间的一种协作机制
。
谈到线程我们经常想到的是线程间的竞争(race)
,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)
), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()
);
或可以指定wait的时间,等时间到了自动唤醒;
在有多个线程进行等待时,如果需要,可以使用 notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
- wait:线程不再活动,不再参与调度,进入
wait set
中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它还要等着别的线程执行一个特别的动作
,也即“通知(notify)
”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue
)中。 - notify:则选取所通知对象的 wait set 中的一个线程释放。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
🗳️注意:
被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
- 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
二、案例
(1)案例
🌋案例描述:
使用两个线程打印 1-100。线程1, 线程2 交替打印。
🍰分析
1、创建线程
这里使用实现的方式创建线程。
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {}
}
在run
方法里面写上逻辑代码:
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
在main
方法中创建两个线程,如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}
关于number的问题,需要考虑同步。
在if中加一个sleep
,将问题放大。
目前的代码如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
输出:
可以看到打印出了错误信息。
2、解决线程安全问题
现在我们使用同步代码块
来解决线程安全问题。
将操作number的代码用synchronized
包裹起来,就是下面蓝色部分:
用synchronized
包裹的时候,不能包裹少了,那样会有安全问题。
若是包裹多了,串行的场景就会更多,导致效率变低。
所以,操作共享数据的代码,不能包裹多,也不能少。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}}
}
🍺输出
可以看到现在的线程是安全的。
3、等待
紧接着需要考虑下一个问题,需要将两个线程交替打印。
🎲如何实现交替?
比如线程1进入同步代码块,然后打印结束,出了同步代码块。
此时线程1需要进入阻塞状态,让线程2进入同步代码块输出执行,才能保证与线程1交替执行。
如何让线程1处于阻塞,这里需要使用一个方法wait()
,让它处于等待状态。如下:
线程一旦执行wait()
方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用。
wait()
方法有一个异常需要处理一下:
注意,sleep()
不会释放同步监视器,这一点需要注意。
有时候面试会问,wait()
与sleep()
有什么区别?
这就是其中一个区别,sleep()
不会释放对同步监视器的调用,而wait()
会释放对同步监视器的调用。
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
☕注意
一旦线程1进入同步代码块执行到wait()
,就会释放对同步监视器的调用,然后开始等待操作。
线程2就可以拿着同步监视器进入同步代码块了,当线程2执行到wait()
,它也开始等待。
所以两个线程都处于等待状态,只能输出两个数字,然后进入了阻塞状态。
4、唤醒
🎲如何不一直等待下去呢?
Ctrl+P
可以看到wait()
方法还有带参的,可以设置等待时间,就是达到时间自动醒来。如下:
现在我们不使用这个了,因为想让它们交互输出,可以考虑唤醒线程。
线程1进入同步代码块执行到wait()
,然后释放同步监视器,线程2进入同步代码块。
所以我们需要在线程2wait()
之前把线程1给唤醒。
可以在这里写上notify()
,如下:
🍰分析一下现在的情况:
线程1进入同步代码块,碰到了notify()
,发现没有可以唤醒的线程,就继续往后执行。
然后线程1打印了“1”,接下来执行到wait()
,开始等待,并且释放同步监视器。
线程2拿到锁,进入同步代码块,碰到了notify()
,发现线程1在等待,就将线程1叫醒,虽然叫醒了,但是没有用,因为已经没有同步资源了。
线程2就拿着锁继续执行,即使sleep
也不影响自动睡醒之后继续往后执行(sleep不会导致锁被释放),然后输出“2”。
当线程2执行到wait()
,就开始等待了,同时也释放同步监视器。
此时线程1是醒着的状态,从被wait()
的地方继续往后执行,后边要是有代码的话还需要继续执行,然后拿着锁再次进入同步代码块,然后碰到notify()
,将线程2叫醒。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
现在看到的就是交互的场景。
5、同步监视器
🗳️关于同步监视器
这里其实省略了this
,如下:
wait
同理:
notify(); //this.notify();
wait(); //this.wait();
凡是在方法当中,没有写是谁调用的,如果是非静态方法,那就少了this。
若是静态方法,那就是当前类。
这里的this
必须是一样的吗?如下:
我们先来自己定义一个同步监视器,然后将obj
放入:
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全this.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
会报一个异常IllegalMonitorStateException
,这表示当前使用的同步监视器(即obj)和notify()
、wait()
方法的调用者不一致。
所以,它们的调用者,必须是同步监视器。(这里也可以看出,notify
和wait
方法必须要在同步代码块或同步方法中使用,只有这两个结构中才存在同步监视器,Lock里面没有同步监视器)
🚗处理
既然现在的同步监视器是obj,那么就用obj去调用notify
和wait
方法即可。
如下:
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全obj.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {obj.wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
notify
和wait
方法的调用者必须是“同步监视器”。
(2)调用wait和notify需注意的细节
上述案例涉及到三个方法的使用:
wait()
:线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。notify()
:一旦执行此方法,就会唤醒被wait()
的线程中优先级最高的那一个线程。(如果被wait()
的多个线程的优先级相同,则随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
🗳️注意点
- 此三个方法的使用,必须是在同步代码块或同步方法(即
synchronized
结构)中。(超纲:Lock
需要配合Condition
实现线程间的通信,方式更加灵活) - 此三个方法的调用者,必须是同步监视器。否则,会报
IllegalMonitorStateException
异常。 - 此三个方法声明在Object类中。(当初说同步监视器的时候,说到“任何一个对象都可以来充当同步监视器”,那么就意味着任何一个对象都应该有能力去调用这几个方法,这几个方法必定是定义在Object类里面的)
☕调用wait
和notify
需注意的细节总结
1、wait
方法与notify
方法必须要由同一个锁对象调用
。
因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2、wait
方法与notify
方法是属于Object类的方法的。
因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3、wait
方法与notify
方法必须要在同步代码块
或者是同步函数
中使用。
因为:必须要通过锁对象
调用这2个方法。否则会报java.lang.IllegalMonitorStateException
异常。
三、wait与sleep的区别
🎲wait()
和 sleep()
的区别?(常见面试题)
①相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。
②不同点:
- 声明的位置(定义方法所属的类)
wait()
:声明在Object类中。sleep()
:声明在Thread类中,静态的。
- 使用的场景不同(使用范围不同)
wait()
:只能使用在同步代码块或同步方法中。sleep()
:可以在任何需要使用的场景。
- 都在同步结构(同步代码块或同步方法)中使用的时候,是否释放同步监视器的操作不同
wait()
:一旦执行,会释放同步监视器。sleep()
:一旦执行,不会释放同步监视器。
- 结束阻塞的方式(结束等待的方式不同)
wait()
:到达指定时间自动结束阻塞 或 无限等待直到被notify/notifyAll唤醒,结束阻塞。sleep()
:到达指定时间自动结束阻塞。