源码地址:https://github.com/nieandsun/concurrent-study.git
1 wait、notify、notifyAll简单介绍 1.1 使用方法 + 为什么不是Thread类的方法为什么不是Thread类的方法
首先应该明确wait、notify、notifyAll三个方法都是对锁对象的操作
,而锁可以是任何对象。在java的世界中任何对象都属于Object类,因此这三个方法都是Object的方法
, 而不是线程对象Thread的方法。
使用方法
需要注意两点:
(1)这三个方法必须在synchronized关键字包含的临界区
(简单理解,就是代码块)内使用
(2)使用方式为锁对象.方法()
,比如obj.wait();
1.2 什么时候加锁、什么时候释放锁?
必须要明确以下几点:
(1)notify和notifyAll方法不会释放锁
,这两个方法只是通知其他使用该锁当锁
但是在wait状态的线程,可以准备抢锁了
这里还要格外注意一点,其他使用该锁当锁且处于wait状态的线程只有被notify或notifyAll唤醒了,才有资格抢锁
(2)某个锁对象调用wait方法会立即释放当前线程的该对象锁
, 且其他线程通过notify/notifyAll方法通知该线程可以抢该对象锁时,如果当前线程抢到了,会从当前锁的wait方法之后开始执行
— 即从哪里wait,从哪里执行;
(3)在synchronized、wait、notify、notifyAll的组合里
加锁的方式只有一个
即进入同步代码块时加锁;
释放锁的方式有两个: ①锁对象调用wait方法时会释放锁 ;② 走完同步代码块时自动释放锁
1.3 notify、notifyAll的区别
某个锁对象的notify只会唤醒一个
使用该锁当锁且处于wait状态的线程;
某个锁对象的notifyAll方法会把所有
使用该锁当锁且处于wait状态的线程都唤醒;
使用建议:
为了防止某些线程无法被通知到,建议都使用notifyAll。
感觉上学的时候好像就考过下面这两个案例☺☺☺
2.1 案例1 — ABCABC。。。三个线程顺序打印问题 2.1.1 题目三个线程,线程A不停打印A、线程B不停的打印B、线程C不停的打印C,如何通过synchronized、wait、notifyAll(或notify)的组合,使三个线程不停地且顺序地打印出ABCABC。。。
2.1.2 题目分析其实我在《【并发编程】— Thread类中的join方法》这篇文章里用join实现过类似的功能,有兴趣的可以看一下。。。
如果使用synchronized、wait、notifyAll(或notify)的组合的话,这个问题可以归结为下图所示的问题。即:
线程A走完 ,线程B走 —> 线程B走完,线程C走 —》 线程C走完,线程A走 。。。。
以线程A为起点进行分析,可知:
(1)要想线程A走完,线程B接着走,那肯定是线程A释放了线程B所需要的锁,这里设该锁为U,做进一步分析可知:
既然线程B需要线程A释放的锁U,那就意味着此时线程B中的锁U肯定处于wait状态; 同时要想线程A释放了锁U之后,线程B可以被唤醒,线程A还必须得进行锁U的notify或notifyAll(2)同理,要想线程B走完,线程C走,那肯定是线程C有一把处于wait状态的锁,这里设为V,需要线程B进行该锁的notify或notifyAll 并释放
(3)再同理,要想线程C走完,线程A接着走,那肯定是线程A有一把处于wait的锁,这里设为W,需要线程C进行该锁的notify或notifyAll 并释放
用图可以表示成下面的样子:
分析到这里我们可以再提炼一下:
前面的
线程释放后自己要抢到的锁、第二把锁是自己
要notify或notifyAll的锁,对应到每个线程,就可以这样描述
线程A需要两把锁,一把为线程C需要notify(或notifyAll)+ 释放的锁,可以认为该锁为C锁;另一把是自己需要notify(或notifyAll)+释放的锁,可以认为该锁为A锁
同理,线程B需要A线程notify(或notifyAll)+ 释放的锁A锁,自己需要notify(或notifyAll)+释放的B锁
再同理,线程C需要B线程notify(或notifyAll)+ 释放的锁B锁,自己需要notify(或notifyAll)+释放的C锁
分析到这里后,可以将上图改成下面的样子,这样理解起来,我感觉会更好一些:
分析到这里就可以写代码了。
2.1.3 我的答案 codepackage com.nrsc.ch1.base.producer_consumer.ABCABC;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
@Slf4j
@AllArgsConstructor
public class ABCABC implements Runnable {
private String obj;
//前一个线程需要释放,本线程需要wait的锁
private Object prev;
//本线程需要释放,下一个线程需要wait的锁
private Object self;
@Override
public void run() {
int i = 3;
while (i > 0) {
//为了在控制台好看到效果,我这里打印3轮
synchronized (prev) { //抢前面线程的锁
synchronized (self) {// 抢到自己应该释放的锁
System.out.println(obj);
i--;
self.notifyAll(); //唤醒其他线程抢self
}//释放自己应该释放的锁
try {
//走到这里本线程已经释放了自己应该释放的锁,接下来就需要让自己需要等待的锁进行等待就可以了
if (i > 0) { //我最开始没加这个条件,但是测试发现程序没停,其实分析一下就可以知道
//当前面i--使i=0了,其实该线程就已经完成3次打印了,就不需要再等前面的锁了
//因此这里加了该if判断
prev.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Object lockA = new Object();
Object lockB = new Object();
Object lockC = new Object();
//线程A需要等待C线程释放的锁,同时需要释放本线程该释放的锁A
new Thread(new ABCABC("A", lockC, lockA)).start();
Thread.sleep(1); //确保开始时A线程先执行
//线程B需要等待A线程释放的锁,同时需要释放本线程该释放的锁B
new Thread(new ABCABC("B", lockA, lockB)).start();
Thread.sleep(1); //确保开始时B线程第2个执行
//线程C需要等待B线程释放的锁,同时需要释放本线程该释放的锁C
new Thread(new ABCABC("C", lockB, lockC)).start();
}
}
测试结果:
2.2 生产者消费者问题
2.2.1 题目
如下图所示:
(1)有多个生产者,每个生产者都在不断的抢面包厂里的机器生产面包 —> 某个时间段只能有一个生产者进行生产 (2)厂里最多能存储20箱,也就是说当已经有20箱了,各个生产者就不能生产了,需要等待消费者消费了,才能继续生产 (3)消费者也有多个,他们也会抢着去面包厂买面包,但也是某个时间段,只能有一个消费者抢到买面包的资格在以上条件的基础上,写一个多线程程序,保证在生产者不断生产面包的同时,消费者也在不断的购买面包。
(注意:
不能写成生产者先生产了20箱,然后消费者再去消费20箱)
其实我觉得这个很简单,只需要想明白下面的两点肯定就可以把这个代码写出来。
对于生产者
(1)它们要不停地生产,直到面包的箱数大于等于20时,生产者就等待 —> 等着消费者去消费 (2)当面包的箱数小于20时,抢到生产权的生产者就生产,并通知消费者,我刚生产了一个,你们可以再继续消费了对于消费者
(1)他们要不停地消费,知道面包的箱数为0时,它们就等待 —> 等着生产这去生产 (2)当面包的箱数大于0时,抢到消费权的消费者就消费,并通知生产者,我刚消费了一个,你们可以再继续生产了 2.2.3 我的答案 生产者和消费者package com.nrsc.ch1.base.producer_consumer.multi;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BreadProducerAndConsumer2 {
/***面包集合*/
private int i = 0;
/***
* 生产者 ,注意这里锁是当前对象,即this
*/
public synchronized void produceBread() {
//如果大于等于20箱,就等待 --- 如果这里为大于20的话,则20不会进入while,则会生产出21箱,所以这里应为>=
while (i >= 20) {
try {
this.wait();
} catch (InterruptedException e) {
log.error("生产者{},等待出错", Thread.currentThread().getName(), e);
}
}
//如果不到20箱就继续生产
i++; //生产一箱
log.warn("{}生产一箱面包,现有面包{}个", Thread.currentThread().getName(), i);
//生产完,通知消费者进行消费
this.notifyAll();
}
/***
* 消费者
*/
public synchronized void consumeBread() {
//如果没有了就等待
while (i 0,所以进行消费
i--; //消费一箱
log.info("{}消费一个面包,现有面包{}个", Thread.currentThread().getName(), i);
//消费完,通知生产者进行生产
this.notifyAll();
}
}
测试类
package com.nrsc.ch1.base.producer_consumer.multi;
public class MultiTest {
public static void main(String[] args) throws InterruptedException {
BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2();
/***
* 不睡眠几秒,效果不是很好,
* 因此我在
* 生产者线程里睡了12秒 --- 因为我觉得生产面包的时间应该长 ☻☻☻
* 消费者线程里睡了6秒 --- 因为我觉得买面包的时间应该快 ☻☻☻
*/
//生产者线程
for (int i = 0; i {
//每个线程都不停的生产
while (true) {
try {
Thread.sleep(12);
} catch (InterruptedException e) {
e.printStackTrace();
}
pc.produceBread();
}
}, "生产者" + i).start();
}
//消费者线程
for (int i = 0; i {
//每个线程都不停的消费
while (true) {
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
pc.consumeBread();
}
}, "消费者" + i).start();
}
}
}
测试效果如下: