现象:
当开启了多个线程后,出现了生产了一个产品,消费两个产品的情况,也有可能出现生产两个,消费一个的情况。如下图所示:
分析:
假设生产者先获取到了CPU的执行权,生产者有两个线程t1,t2,消费者有两个t3,t4,t1遇到flag(为假),即里面没有数值不用等待,直接生产(即打印),之后将flag改为true,notify之后循环到if判断,flag变为true,此时t1放弃资格,进入等待
此时t2,t3,t4都有可能抢夺到CUP资源,假设t2此时抢到了资源,判断其为true,也进行等待!此时t3抢到了执行权,判断条件
执行完之后(消费完),执行到flag = false,再 进行唤醒,flag变成了false,此时唤醒了t1,t3放弃了资格,t4读到之后也会变成等待,之后t1又继续执行,即再次生产了一次,,此时t1又再次将flag标记为true,唤醒了t2,此时, t1又再次进行判断,因为flag为true,所以只能等待,此时t2不能判断flag,直接又进入生产,就出现了就出现了生产多次的情况
注:此例中生产者默然flag = false时生产,消费者flag = true时消费
原因:
是因为多个线程线程产生之后(t1,t2,t3,t4),t1判断flag输入值之后,将t2唤醒,但是t2无法判断if(flag),就会直接
执行以下的代码,即继续生产(t4无法判断if(!flag),也会直接执行消费的动作),即将if条件判断改成while(flag)循环,这样无论是t2还是t4,遇到while都会去判断flag.
但是用了while循环以后,又会出现一种情况就是所有的线程都会等待,t1把自己同步代码块中的线程(t2)唤 醒了,但是无法唤醒 其他同步中的线程(t3,t4), 所以将notify()方法换成notifyAll(),就会唤醒全部的线程,再有了while循环,线程就会自己
判断是否应该执行
总结:
当程序中只有一个生产者,和一个消费者时,同步代码块中使用的判断和唤醒方式是if(flag),和notify();—>即单一线程
防止出现生产一个,消费多个的办法:
当程序中有多个生产者和消费者时,同步代码块中使用的判断和唤醒方式是while(flag),和notifyAll();—>即多 个线程
代码示例:
package com.itheima.day12;
public class ProducerConsumerDemo {
public static void main(String[] args) {
ResourcePC r = new ResourcePC();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//创建资源类
class ResourcePC{
private String name;
//定义一个计数器
private int count = 1;
private boolean flag = false;
//涉及到安全问题用同步synchronized修饰函数,即用同步函数
public synchronized void set(String name){
while(flag)
try{
this.wait();
}catch(Exception e){
}
this.name = name+" "+count++;
System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name);
flag = true;
//this.notify();
this.notifyAll();
}
//定义打印方法
public synchronized void PrintOut(){
while(!flag)
try{this.wait();}catch(Exception e){} //注意实际开发中不允许这样写,此处是为了对比
System.out.println(Thread.currentThread().getName()+"..消费者.."+this.name);
flag = false;
//this.notify();
this.notifyAll();
}
}
class Producer implements Runnable{
private ResourcePC res;
Producer(ResourcePC res){
this.res = res;
}
public void run(){
while(true){
res.set("+商品+");
}
}
}
class Consumer implements Runnable{
private ResourcePC res;
Consumer(ResourcePC res){
this.res = res;
}
public void run (){
while(true){
res.PrintOut();
}
}
}
线程间通信:
新的需求:
用notifyAll()方法唤醒所有的线程之后,现在只要求唤醒对方的线程(消费者中的线程t3,t4),
不用唤醒自身中的线程(生产者中的t2线程)
在Java的API文档,有一个java.util.concurrent.locks的包中有一个Lock接口 ,其中描述道:
“Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,此实现允许更灵活的结
构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象”。
由此可见:
Lock替代了synchronized,
Condition替代了Object类中的监视器方法,wait(),notify()和notifyAll(),
对应的变成了await(),signal()和signalAll()
Condition的由来 :
把wait(),notify()封装成Condition对象,Condition应该怎么来呢,如何获取?要通过锁来获取
因为wait(),notify()本应定义在同步代码(synchronized)块当中,同步中有锁,每一个wait(),
notify(),都要标识自己所属的锁,现在同步变成了Lock,而wait,notify变成了Condition
Lock接口当中就定义了newCondition();的方法
则:
Condition cons = lock.newCondition();
分析:
在lock.lock()之后 ,如果拿到锁了,在执行被锁代码中condition.await();抛了异常,之后
功能会结束 ,lock.unlock();没有执行到,即所没有被释放,其他线程无法进来功能,而lock.unlock();
是一定要背执行的,此时就可以用try-finally
用Lock和同步 synchronized会达到同样的效果,此时还没有达到新的需求的效果,即就是只唤醒对方的线程(t3,t4),
不用唤醒自己当中的其他线程(t2)
此时,就会用到Lock具备的新的特性:
一个锁上可以拥有(支持)多个相关的Condition对象
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
condition_pro.signal();//唤醒生产者(t1,t2线程),此时它只唤醒t1或者t2中的一个,
原理:
生产者代码里con唤醒消费者中的线程(t3,t4),消费者功能代码中 pro唤醒生产者(t1,t2),于是就实现了唤醒
对方的线程,而不唤醒自己线程的需求
等待唤醒机制示意图:
代码示例:
package com.itheima.day12;
import java.util.concurrent.locks.Condition;
public class ProducerConsumerDemo2 {
public static void main(String[] args) {
ResourcePC2 r = new ResourcePC2();
Producer2 pro = new Producer2(r);
Consumer2 con = new Consumer2(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class ResourcePC2{
private String name;
private int count = 1;
private boolean flag = false;
//ReentrantLock是Lock的一个子类,也叫实现类,它具有自己的构造函数
private Lock lock = new ReentrantLock();
//通过锁产生了一个具备wait,notify功能的实例对象
//private Condition condition = lock.newCondition();//Condition对象的由来
//使用Lock的新特性:一个锁上可以拥有(支持)多个相关的Condition对象,升级版的为:
private Condition condition_pro = lock.newCondition(); //生产者
private Condition condition_con = lock.newCondition(); //消费者
public void set(String name) throwsInterruptedException {
//获取锁
lock.lock();
try {
//判断标记后若为true就需要等待:
while(flag)
//condition.await(); //抛了异常,要try一下 。(未用Lock新特性之前)
condition_pro.await(); //若生产者(t1,t2线程)生产完之后,等待,唤醒消费者(用新特性)
this.name = name+" "+count++;
System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name);
flag = true;
//condition.signal(); //唤醒 (未使用Lock新特性)
// conditio.signalAll(); //唤醒所有线程
condition_con.signal();//唤醒消费者(t3,t4线程)
}finally{
lock.unlock();//释放锁
}
}
public void PrintOut() throws InterruptedException{
lock.lock();
try {
while(!flag)
//condition.await();
condition_con.await();
System.out.println(Thread.currentThread().getName()+"..消费者.."+this.name);
flag = false;
//condition.signal();//只是唤醒了部分线程,运行会出现等待,卡死的现象,所以将其换成signalAll();
//condition.signalAll();
//唤醒生产者(t1,t2线程),此时它只唤醒t1或者t2中的一个
condition_pro.signal();
}finally{
lock.unlock();
}
}
}
class Producer2 implementsRunnable{
private ResourcePC2 res;
Producer2(ResourcePC2 res){
this.res = res;
}
public void run(){
while(true){
try {
res.set("+商品+");
} catch (InterruptedException e) {
}
}
}
}
class Consumer2 implements Runnable{
private ResourcePC2 res;
Consumer2(ResourcePC2 res){
this.res = res;
}
public void run (){
while(true){
try {
res.PrintOut();
} catch (InterruptedException e) {
}
}
}
}
总结:
JDK1.5之后,就提供了多线程升级 的解决方案:(提供了显示的锁机制以及显示的锁对象上的等待唤醒操作制)
将同步synchronized替换成显示的Lock操作
将Object中的wait,notify,notifyAll,封装成了Condition对象
该对象可以Lock锁,进行获取
该示例中,实行了本方只唤醒对方线程的操作,以前创建一个锁,就需要进行同步,嵌套之后容易形成死 锁, 有了Lock以后,一个锁可以拥有多组(wait,notify)