等待唤醒机制的应用;一次生产一次消费
一,前置知识
在线程通信中,对于synchronized的锁对象来说,每个锁对象都有一个线程池;其次对于传统的线程通信手段就是:
wait()方法:导致当前线程等待,该线程进入线程池知道notify()和notifyAll()唤醒,且等待后当前线程会释放锁对象;
notify()方法:唤醒此锁对象上等待的线程,即从该线程池唤醒且是任意唤醒一个等待线程,只有唤醒某个等待线程后,才可以执行被唤醒的线程(注意,不是刚唤醒就立刻执行,还得看CPU),且从被wait()代码的下边继续往下运行;
notifyAll()方法:唤醒此锁对象的所有等待线程。
tips:这三个方法都是Object的方法。
二,生产者消费者模型:
特点:通过在资源里定义旗标(flag),在方法任务里操作flag和同步函数,来实现。
package ThreadDemo.Message;
/*
* 单生产单消费
*/
public class ProductCustome {
public static void main(String[] args) {
// TODO Auto-generated method stub
Res r=new Res();
Product p=new Product(r);
Custome c=new Custome(r);
Thread t1=new Thread(p);
Thread t2=new Thread(c);
t1.start();
t2.start();
}
}
class Res {
private String name;
private int count=1;
boolean flag=false;//假 为没有资源,真 为有资源
public synchronized void set(String name) {//生产
if(flag) {
try {
wait();//生产者阻塞。消费者开始
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}//生产者阻塞,消费者执行
}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+" 生产者 "+this.name);
flag=true;
notify();//释放消费者
}
public synchronized void out() {//消费
if(!flag) {
try {
wait();//消费者阻塞,生产者执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" 消费者 "+this.name);
flag=false;
notify();//释放生产者
}
}
class Product implements Runnable{
private Res r;
public Product(Res r) {
this.r=r;
}
public void run() {
while(true) {
r.set("烤鸭");
}
}
}
class Custome implements Runnable{
private Res r;
public Custome(Res r) {
this.r=r;
}
public void run() {
while(true) {
r.out();
}
}
}
结果:生产消费依次进行。
Thread-0 生产者 烤鸭20879
Thread-1 消费者 烤鸭20879
Thread-0 生产者 烤鸭20880
Thread-1 消费者 烤鸭20880
Thread-0 生产者 烤鸭20881
Thread-1 消费者 烤鸭20881
Thread-0 生产者 烤鸭20882
Thread-1 消费者 烤鸭20882
但是:
若是在多生产者多消费者中上述同步写法依然会造成线程安全问题。输出连续生产和消费的情况,以及还可能导致死锁。
输出连续生产和消费的情况因为:现在是一次只能唤醒一个线程,若现有a,aa两生产者,b,bb两消费者,当flag=false,a生产者正常运行,flag=true且输出生产,但如果此时a线程还占有执行权(时间片未完)继续调用生产函数,由于flag=true,a就wait了,进入线程池。cpu切换执行权到aa,aa也同理进入线程池,接着CPU切换执行权给b,此时b消费者正常执行消费完后,输出消费,flag=false,且释放了a线程,这下出问题了:a线程从被wait下面继续执行,flag=true,输出生产,再释放本方aa线程,又接着往下走,输出了生产。问题就在于被释放后由于代码从被wait接着执行,不能再次判断 if 导致的,解决办法是在判断if(flag)改为while(flag).
而死锁是因为:就是由于一次只能唤醒一个线程导致的,如果a输出生产flag=true,再次调用生产函数,自己等待加入线程池,接着aa也因为条件被等待加入线程池,b输出消费flag=false,释放a,但此时还是b占有执行权,b被加入线程池,接着bb,由于flag=false,也被加入线程池,此时线程池里有(aa,b,bb),轮到a时,此时flag不满足while条件,继续执行,输出生产flag=true,并且恰好唤醒的是本方aa,a还占有CPU(时间片没完),也被加入线程池,此时线程池(a,b,bb),而轮到aa时,由于flag=true也被加入线程池,就这样线程池里(a,b,aa,bb),就死锁了。那么解决办法就是notifyAll(),每次唤醒全都唤醒:即使唤醒的时本方,也会因为while()判断不会出现多输出,而唤醒对方就不会锁了。
这里的线程池只是属于r对象的。
package ThreadDemo.Message;
/*
* 多生产者多消费者
* if判断标记,只有一次会导致不该运行的线程运行了。出现了数据错误的情况
* while判断标记,解决了线程获取执行权后是否要运行(由于wait唤醒后直接往下执行,不再执行判断,会多生产或多消费的问题)
* nofity:只能一次唤醒一个线程,如果本方唤醒了本方没有意义,而且由于while判断标记(又锁住)+nofity会导致死锁
* nofityAll:解决了死锁问题,(即使本方唤醒了本方,会再锁住,让对方唤醒继续执行)
*/
public class ProductCustome2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Res2 r=new Res2();
Product2 p=new Product2(r);
Custome2 c=new Custome2(r);
Thread t0=new Thread(p);
Thread t1=new Thread(p);
Thread t2=new Thread(c);
Thread t3=new Thread(c);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
class Res2 {
private String name;
private int count=1;
boolean flag=false;//假 为没有资源,真 为有资源
public synchronized void set(String name) {//生产
while(flag) {
try {
wait();//生产者阻塞。消费者开始
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}//生产者阻塞,消费者执行
}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+" 生产者 "+this.name);
flag=true;
notifyAll();//释放消费者
}
public synchronized void out() {//消费
while(!flag) {
try {
wait();//消费者阻塞,生产者执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" 消费者 "+this.name);
flag=false;
notifyAll();//释放生产者
}
}
class Product2 implements Runnable{
private Res2 r;
public Product2(Res2 r) {
this.r=r;
}
public void run() {
while(true) {
r.set("烤鸭");
}
}
}
class Custome2 implements Runnable{
private Res2 r;
public Custome2(Res2 r) {
this.r=r;
}
public void run() {
while(true) {
r.out();
}
}
}
输出和单生产单消费一样和谐就不上结果了。
三.tips:
1 没加锁时当任务结束后不一定会立刻切换CPU执行权,因为时间片没到,所以会出现连续的一片同样的情况
2.被唤醒后不会立即切换线程,道理同上。