线程并发协作(生产者/消费者模式)
负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
消费者不能直接使用生产者的数据,它们之间有个“仓库”。生产者将生产好的数据放入“仓库”,消费者从“仓库”拿要处理的数据
- 注意:
- 仓库作为消费者与生产者之间的缓冲区,使生产与消费的线程分离大道解耦的效果
- 解决生产消费过载,生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据
实例需求
- 《wait/notify实现生产者和消费者程序》
- 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
- 对某一个对象(枪膛)进行操作,其最大容量是10颗子弹,
- 生产者线程是一个压入线程,它不断向枪膛中压入子弹,
- 消费者线程是一个射出线程,它不断从枪膛中射出子弹。
/**
* @author LHW
* @date 2020/12/25.
* Description:子弹实体类
*/
public class Bullet {
public int id;
}
/**
* @author LHW
* @date 2020/12/25.
* Description:枪膛 压入发射子弹
*/
public class Clip {
public int index = 0;//压入与发射的标志位
public Bullet[] bullets = new Bullet[10];//缓存子弹弹夹
//压入子弹入枪膛
public synchronized void pushBullet(Bullet bullet) {
try {
while (index == bullets.length) {
System.out.println("pushBullet 弹夹已满进入等待。。。。。index=" + index);
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notify();//唤醒等待线程
bullets[index] = bullet;
index++;
}
//枪膛射出子弹
public synchronized Bullet shootBullet() {
try {
while (index == 0) {
System.out.println("shootBullet 枪膛为空进入等待。。。。。index=" + index);
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notify();
index--;
return bullets[index];
}
}
/**
* @author LHW
* @date 2020/12/25.
* Description:
* 《wait/notify实现生产者和消费者程序》
* 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
* 对某一个对象(枪膛)进行操作,其最大容量是10颗子弹,
* 生产者线程是一个压入线程,它不断向枪膛中压入子弹,
* 消费者线程是一个射出线程,它不断从枪膛中射出子弹。
*/
public class ShootingMain {
public static void main(String[] args) {
Clip mClip = new Clip();
new PushThread(mClip).start();
new ShootThread(mClip).start();
}
static class PushThread extends Thread {
Clip mClip;
PushThread(Clip clip) {
mClip = clip;
}
@Override
public void run() {
super.run();
for (int i = 0; i < mClip.bullets.length; i++) {
Bullet bullet = new Bullet();
bullet.id = i;
mClip.pushBullet(bullet);
System.out.println("PushThread 子弹压入枪膛。。。" + bullet.id);
//如果这里不执行睡眠等待 生产会压入所有子弹后执行消费线程的射出子弹
SleepTools.ms(1000);//睡眠一秒 阻塞生产线程 执行消费线程
}
}
}
static class ShootThread extends Thread {
Clip mClip;
ShootThread(Clip clip) {
mClip = clip;
}
@Override
public void run() {
super.run();
for (int i = 0; i < mClip.bullets.length; i++) {
Bullet bullet = mClip.shootBullet();
System.out.println("ShootThread 子弹射出枪膛。。。" + bullet.id);
SleepTools.ms(1000);//睡眠一秒 阻塞消费线程 执行生产线程
}
}
}
}
结果输出
PushThread 子弹压入枪膛。。。0
ShootThread 子弹射出枪膛。。。0
shootBullet 枪膛为空进入等待。。。。。index=0
PushThread 子弹压入枪膛。。。1
ShootThread 子弹射出枪膛。。。1
shootBullet 枪膛为空进入等待。。。。。index=0
PushThread 子弹压入枪膛。。。2
ShootThread 子弹射出枪膛。。。2
PushThread 子弹压入枪膛。。。3
ShootThread 子弹射出枪膛。。。3
PushThread 子弹压入枪膛。。。4
ShootThread 子弹射出枪膛。。。4
shootBullet 枪膛为空进入等待。。。。。index=0
PushThread 子弹压入枪膛。。。5
ShootThread 子弹射出枪膛。。。5
PushThread 子弹压入枪膛。。。6
ShootThread 子弹射出枪膛。。。6
shootBullet 枪膛为空进入等待。。。。。index=0
PushThread 子弹压入枪膛。。。7
ShootThread 子弹射出枪膛。。。7
shootBullet 枪膛为空进入等待。。。。。index=0
PushThread 子弹压入枪膛。。。8
ShootThread 子弹射出枪膛。。。8
PushThread 子弹压入枪膛。。。9
ShootThread 子弹射出枪膛。。。9
总结
消费者/生产模式通常用于如下情景:
- 1.生产与消费共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 2.生产者没有生产产品前即资源为空时,消费者进入等待,否则通知消费者进行消费
- 3.消费者消费后要通知生产者已消费完,需要通知生产者进行生产产品以供消费
线程直接的协作
- 协作(等待 / 通知机制)情景
是指一个线程 A 调用了对象 User 的 wait()方法进入等待状态,而另一个线程 B
调用了对象User 的 notify()或者 notifyAll()方法,线程A 收到通知后从对象User 的wait()方法返回,进而执行后续操作。上述两个线程通过对象User来完成交互,而对象上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
- notify():
- 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程
获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
- notifyAll():
通知所有等待在该对象上的线程
- wait()
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断
才会返回.需要注意,调用 wait()方法后,会释放对象的锁
- wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有
通知就超时返回
- wait (long,int)
-对于超时时间更细粒度的控制,可以达到纳秒
注意:
以上方法均是java.lang.Object类的方法;
都只能在同步方法或者同步代码块中使用,否则会抛出异常。
等待和通知的标准范式
等待方遵循如下原则
- 获取对象的锁
- 如果条件不满足,那么调用的对象的wait()方法,被通知后仍要检查条件
- 条件满足则执行对应的逻辑
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则
- 获取对象的锁
- 改变条件
- 通知所有等待在对象上的线程
synchronized(对象){
改变条件
对象.notifyAll();
}
在调用 wait () 、notify() 系列方法 之前,线程必须要获得该对象的对象级
别锁,即只能在同步方法或同步块中调用 wait ()方法 、notify() 系列方法,进
入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞
争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized
代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会
继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的
线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕
注意事项:
1.notify和notifyAll应该用谁?
- 尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我
们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
2.调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
- yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
- 调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新
去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。 - 调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才
会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行