Files
java-book/javaSE2/day06/day06【线程、同步】.md
2025-08-27 14:47:26 +08:00

763 lines
30 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# day06 【线程、同步】
### 主要内容
- 线程
- 同步
- 线程状态
### 教学目标
- [ ] 能够描述Java中多线程运行原理
- [ ] 能够使用继承类的方式创建多线程
- [ ] 能够使用实现接口的方式创建多线程
- [ ] 能够说出实现接口方式的好处
- [ ] 能够解释安全问题的出现的原因
- [ ] 能够使用同步代码块解决线程安全问题
- [ ] 能够使用同步方法解决线程安全问题
- [ ] 能够理解线程通信概念
- [ ] 能够理解等待唤醒机制
- [ ] 能够说出线程6个状态的名称
## 第一章 线程
### 1.1 多线程原理
昨天的时候我们已经写过一版多线程的代码,很多同学对原理不是很清楚,那么我们今天先画个多线程执行时序图来体现一下多线程程序的执行流程。
代码如下:
自定义线程类:
~~~javascript
public class MyThread extends Thread{
/*
* 利用继承中的特点
* 将线程名称传递 进行设置
*/
public MyThread(String name){
super(name);
}
/*
* 重写run方法
* 定义线程要执行的代码
*/
public void run(){
for (int i = 0; i < 20; i++) {
//getName()方法 来自父亲
System.out.println(getName()+i);
}
}
}
~~~
测试类
~~~java
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
}
}
~~~
流程图
![](img\线程流程图.png)
程序启动运行main时候java虚拟机启动一个进程主线程main在main()调用时候被创建随着调用mt的对象的start方法另外一个新的线程也启动了这样整个应用就在多线程下运行
通过这张图我们可以很清晰的看到多线程的执行流程那么为什么可以完成并发执行呢我们再来讲一讲原理
多线程执行时到底在内存中是如何运行的呢以上个程序为例进行图解说明
多线程执行时在栈内存中其实**每一个执行线程都有一片自己所属的栈内存空间**。进行方法的压栈和弹栈
![](img\栈内存原理图.bmp)
当执行线程的任务结束了线程自动在栈内存中释放了但是当所有的执行线程都结束了那么进程就结束了
### 1.2 Thread类
在上一天内容中我们已经可以完成最基本的线程开启那么在我们完成操作过程中用到了`java.lang.Thread`API中该类中定义了有关线程的一些方法具体如下
**构造方法:**
* `public Thread()`:分配一个新的线程对象
* `public Thread(String name)`:分配一个指定名字的新的线程对象
* `public Thread(Runnable target)`:分配一个带有指定目标新的线程对象
* `public Thread(Runnable target,String name)`:分配一个带有指定目标新的线程对象并指定名字
**常用方法:**
* `public String getName()`:获取当前线程名称
* `public void start()`:导致此线程开始执行; Java虚拟机调用此线程的run方法
* `public void run()`:此线程要执行的任务在此处定义代码
* `public static void sleep(long millis)`:使当前正在执行的线程以指定的毫秒数暂停暂时停止执行)。
* `public static Thread currentThread() `:返回对当前正在执行的线程对象的引用
翻阅API后得知创建线程的方式总共有两种一种是继承Thread类方式一种是实现Runnable接口方式方式一我们上一天已经完成接下来讲解方式二实现的方式
### 1.3 创建线程方式二
采用`java.lang.Runnable`也是非常常见的一种我们只需要重写run方法即可
步骤如下
1. 定义Runnable接口的实现类并重写该接口的run()方法该run()方法的方法体同样是该线程的线程执行体
2. 创建Runnable实现类的实例并以此实例作为Thread的target来创建Thread对象该Thread对象才是真正的线程对象
3. 调用线程对象的start()方法来启动线程
代码如下
~~~java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
~~~
~~~java
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
~~~
通过实现Runnable接口使得该类有了多线程类的特征run()方法是多线程程序的一个执行目标所有的多线程代码都在run方法里面Thread类实际上也是实现了Runnable接口的类
在启动的多线程的时候需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象然后调用Thread对象的start()方法来运行多线程代码
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的因此不管是继承Thread类还是实现Runnable接口来实现多线程最终还是通过Thread的对象的API来控制线程的熟悉Thread类的API是进行多线程编程的基础
> tips:Runnable对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例只是该Thread线程负责执行其target的run()方法。
### 1.4 Thread和Runnable的区别
**A避免了Java单继承的局限性**
说明如果使用方式一那么在Java中一个类只能有一个直接父类如果一个类已经继承其他的父类那么当前这个类中假如有需要多线程操作的代码这时这个类是无法再继承Thread类的这样就会导致当前这个类中的某些需要多线程执行的任务代码就无法被线程去执行
**B把线程代码和任务的代码分离解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好;**
说明Thread类是专门负责描述线程本身的Thread类可以对线程进行各种各样的操作如果使用第一种方式那么把线程要执行的任务也交给了Thread类这样就会导致操作线程本身的功能和线程要执行的任务功能严重的耦合在一起
但是方式二自定义一个类来实现Runnable接口这样就把任务抽取到Runnable接口中在这个接口中定义线程需要执行的任务的规则当需要明确线程的任务时我们就让这个类实现Runnable接口只要实现Runnable接口的类就相当于明确了线程需要执行的任务
当一个类实现Runnable接口就相当于有了线程的任务可是还没有线程本身这个对象这时我们就可以直接使用Thread这个类创建出线程然后把任务交给线程这样就达到任务和线程的分离以及结合
### 1.5 匿名内部类方式实现线程的创建
使用线程的内匿名内部类方式可以方便的实现每个线程执行不同的线程任务操作
使用匿名内部类的方式实现Runnable接口重新Runnable接口中的run方法
~~~java
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// }; //---这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
~~~
## 第二章 线程安全
### 2.1 线程安全
如果有多个线程在同时运行而这些线程可能会同时运行这段代码程序每次运行结果和单线程运行的结果是一样的而且其他的变量的值也和预期的是一样的就是线程安全的
我们通过一个案例演示线程的安全问题
电影院要卖票我们模拟电影院的卖票过程假设要播放的电影是 葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口实现多个窗口同时卖 葫芦娃大战奥特曼这场电影票(多个窗口一起卖这100张票)
需要窗口采用线程对象来模拟需要票Runnable接口子类来模拟
模拟票
~~~java
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
~~~
测试类:
~~~java
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
~~~
结果中有一部分这样现象:
![](img\线程安全问题.png)
发现程序出现了两个问题:
1. 相同的票数,比如5这张票被卖了两回。
2. 不存在的票比如0票与-1票是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
> 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
### 2.2 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题Java中提供了同步机制(**synchronized**)来解决。
根据案例简述:
~~~
窗口1线程进入操作的时候窗口2和窗口3线程只能在外等着窗口1操作结束窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候其他线程不能修改该资源等待修改完毕同步之后才能去抢夺CPU资源完成对应的操作保证了数据的同步性解决了线程不安全的现象。
~~~
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
1. 同步代码块。
2. 同步方法。
3. 锁机制。
### 2.3 同步代码块
* **同步代码块**`synchronized`关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
~~~java
synchronized(同步锁){
需要同步操作的代码
}
~~~
**同步锁**:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
1. 锁对象 可以是任意类型。
2. 多个线程对象 要使用同一把锁。
> 注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
}
~~~
当使用了同步代码块后,上述的线程的安全问题,解决了。
### 2.4 同步方法
* **同步方法**:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
~~~java
public synchronized void method(){
可能会产生线程安全问题的代码
}
~~~
> 同步锁是谁?
>
> 对于非static方法,同步锁就是this。
>
> 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
~~~
### 2.5 Lock锁
`java.util.concurrent.locks.Lock`机制提供了比**synchronized**代码块和**synchronized**方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁加锁与释放锁方法化了如下
* `public void lock() `:加同步锁。
* `public void unlock()`:释放同步锁。
使用如下:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
~~~
## 第三章 等待唤醒机制
### 3.1 线程间通信
**概念:**多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如线程A用来生成包子的线程B用来吃包子的包子可以理解为同一资源线程A与线程B处理的动作一个是生产一个是消费那么线程A与线程B之间就存在线程通信问题。
**为什么要处理线程间通信:**
多个线程并发执行时, 在默认情况下CPU是随机切换线程的当我们需要多个线程来共同完成一件任务并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
**如何保证线程间通信有效利用资源:**
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— **等待唤醒机制。**
### 3.2 等待唤醒机制
**什么是等待唤醒机制**
这是多个线程间的一种**协作**机制。谈到线程我们经常想到的是线程间的**竞争race**,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(**wait()** 等待其他线程执行完他们的指定代码过后 再将其唤醒(**notify()**;在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
**等待唤醒中的方法**
等待唤醒机制就是用于解决线程间通信的问题的使用到的3个方法的含义如下
1. wait线程不再活动不再参与调度进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要执行一个**特别的动作**,也即是“**通知notify**”在这个对象上等待的线程从wait set 中释放出来重新进入到调度队列ready queue
2. notify则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
3. notifyAll则释放所通知对象的 wait set 上的全部线程。
> 注意:
>
> 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
>
> 总结如下:
>
> - 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
> - 否则,从 wait set 出来,又进入 entry set线程就从 WAITING 状态又变成 BLOCKED 状态
**调用wait和notify方法需要注意的细节**
1. wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2. wait方法与notify方法是属于Object类的方法的。因为锁对象可以是任意对象而任意对象的所属类都是继承了Object类的。
3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为必须要通过锁对象调用这2个方法。
### 3.3 生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
```java
包子铺线程生产包子吃货线程消费包子当包子没有时包子状态为false吃货线程等待包子铺线程生产包子即包子状态为true并通知吃货线程解除吃货的等待状态,因为已经有包子了那么包子铺线程进入等待状态接下来吃货线程能否进一步执行则取决于锁的获取情况如果吃货获取到锁那么就执行吃包子动作包子吃完包子状态为false并通知包子铺线程解除包子铺的等待状态,吃货线程进入等待包子铺线程能否进一步执行则取决于锁的获取情况
```
**代码演示:**
包子资源类:
```java
public class BaoZi {
String pier ;
String xianer ;
boolean flag = false ;//包子资源 是否存在 包子资源状态
}
```
吃货线程类:
```java
public class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
while(true){
synchronized (bz){
if(bz.flag == false){//没包子
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag = false;
bz.notify();
}
}
}
}
```
包子铺线程类:
```java
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
int count = 0;
//造包子
while(true){
//同步
synchronized (bz){
if(bz.flag == true){//包子资源 存在
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 没有包子 造包子
System.out.println("包子铺开始做包子");
if(count%2 == 0){
// 冰皮 五仁
bz.pier = "冰皮";
bz.xianer = "五仁";
}else{
// 薄皮 牛肉大葱
bz.pier = "薄皮";
bz.xianer = "牛肉大葱";
}
count++;
bz.flag=true;
System.out.println("包子造好了:"+bz.pier+bz.xianer);
System.out.println("吃货来吃吧");
//唤醒等待线程 (吃货)
bz.notify();
}
}
}
}
```
测试类:
```java
public class Demo {
public static void main(String[] args) {
//等待唤醒案例
BaoZi bz = new BaoZi();
ChiHuo ch = new ChiHuo("吃货",bz);
BaoZiPu bzp = new BaoZiPu("包子铺",bz);
ch.start();
bzp.start();
}
}
```
执行效果:
```java
包子铺开始做包子
包子造好了冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子
包子铺开始做包子
包子造好了冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
```
## 第四章 线程状态
### 4.1 线程状态概述
当线程被创建并启动以后它既不是一启动就进入了执行状态也不是一直处于执行状态。在线程的生命周期中有几种状态呢在API中`java.lang.Thread.State`这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析
| 线程状态 | 导致状态发生条件 |
| ------------------- | ---------------------------------------- |
| NEW(新建) | 线程刚被创建但是并未启动。还没调用start方法。 |
| Runnable(可运行) | 线程可以在java虚拟机中运行的状态可能正在运行自己代码也可能没有这取决于操作系统处理器。 |
| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁而该对象锁被其他的线程持有则该线程进入Blocked状态当该线程持有锁时该线程将变成Runnable状态。 |
| Waiting(无限等待) | 一个线程在等待另一个线程执行一个唤醒动作时该线程进入Waiting状态。进入这个状态后是不能自动唤醒的必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
| Timed Waiting(计时等待) | 同waiting状态有几个方法有超时参数调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
| Teminated(被终止) | 因为run方法正常退出而死亡或者因为没有捕获的异常终止了run方法而死亡。 |
我们不需要去研究这几种状态的实现原理我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢新建与被终止还是很容易理解的我们就研究一下线程从Runnable可运行状态与非运行状态之间的转换问题。
### 4.2 Timed Waiting计时等待
Timed Waiting在API中的描述为一个正在限时等待另一个线程执行一个唤醒动作的线程处于这一状态。单独的去理解这句话真是玄之又玄其实我们在之前的操作中已经接触过这个状态了在哪里呢
在我们写卖票的案例中为了减少线程执行太快现象不明显等问题我们在run方法中添加了sleep语句这样就强制当前正在执行的线程休眠**暂停执行**),以“减慢线程”。
其实当我们调用了sleep方法之后当前执行的线程就进入到“休眠状态”其实就是所谓的Timed Waiting(计时等待),那么我们通过一个案例加深对该状态的一个理解。
**实现一个计数器计数到100在每个数字之间暂停1秒每隔10个数字输出一个字符串**
代码:
~~~java
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("-------" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 线程睡眠1秒\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
~~~
通过案例可以发现sleep方法的使用还是很简单的我们需要记住下面几点
1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法单独的线程也可以调用不一定非要有协作关系
2. 为了让其他线程有机会执行可以将Thread.sleep()的调用**放线程run()之内**。这样才能保证该线程执行过程中会睡眠
3. sleep与锁无关线程睡眠到期自动苏醒并返回到Runnable可运行状态
> 小提示sleep()中指定的时间是线程不会运行的最短时间。因此sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
Timed Waiting 线程状态图![](img\计时等待.png)
### 4.3 BLOCKED锁阻塞
Blocked状态在API中的介绍为一个正在阻塞等待一个监视器锁锁对象的线程处于这一状态
我们已经学完同步机制那么这个状态是非常好理解的了比如线程A与线程B代码中使用同一锁如果线程A获取到锁线程A进入到Runnable状态那么线程B就进入到Blocked锁阻塞状态
这是由Runnable状态进入Blocked状态除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态而这部分内容作为扩充知识点带领大家了解一下
Blocked 线程状态图![](img\锁阻塞.png)
### 4.4 Waiting无限等待
**Wating状态**一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态
那么我们之前遇到过这种状态吗答案是并没有但并不妨碍我们进行一个简单深入的了解我们通过一段代码即生产者和消费者线程来学习一下
注意
让线程无线等待我们需要使用wait方法而唤醒线程应该使用notify(唤醒单个线程)或者notifyAll(唤醒所有线程)方法所以我们先熟悉下这几个方法
A:wait()让线程等待
B:notify()唤醒某个等待中的线程
C:notifyAll()唤醒所有等待的线程
通过查阅API我们发现这三个方法都位于Object类中并不是Thread类中
问题为什么这几个方法要定义在Object类中呢
思考线程的等待和唤醒应该由谁来控制
线程有很多控制线程的一定是跟多个线程都有关系的对象--------锁对象
锁对象本来就是控制线程运行的因此线程的等待和唤醒就交给锁来控制
线程等待和唤醒应该由锁来完成
锁对象可以是任意的任意对象都可以调用这几个方法任意对象都可以调用的方法必须定义在Object类中
Waiting 线程状态图![](img\无限等待.png)
### 4.5 补充知识点
到此为止我们已经对线程状态有了基本的认识想要有更多的了解详情可以见下图
![](img\线程状态图.png)
> 一条有意思的tips:
>
> 我们在翻阅API的时候会发现Timed Waiting计时等待 与 Waiting无限等待 状态联系还是很紧密的比如Waiting无限等待 状态中wait方法是空参的而timed waiting计时等待 中wait方法是带参的。这种带参的方法其实是一种倒计时操作相当于我们生活中的小闹钟我们设定好时间到时通知可是如果提前得到唤醒通知那么设定好时间在通知也就显得多此一举了那么这种设计方案其实是一举两得。如果没有得到唤醒通知那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来如果在倒计时期间得到唤醒通知那么线程从Timed Waiting状态立刻唤醒。