1. 生产者和消费者案例
以下为生产者和消费者共享店员进行进货和售货的案例,生产者在库存已满时也不停进货,消费者在库存为0时也不停售货。
那么在实际生产中,就会造成生产者在库存已满时进货,造成数据丢失。消费者也会在不停的消费。
/** * 生产者和消费者案例 * @author xiaobin * @date 2018/3/11 */
public class TestProductAndConsumer {
public static void main(String[] args) {
//在没有使用模式时
Clerk clerk = new Clerk() ;
Productor pro = new Productor(clerk);
Consumer cus = new Consumer(clerk);
new Thread(pro, "生产者A").start();
new Thread(cus, "消费者B").start();
}
}
//店员
class Clerk {
private int product = 0 ;
//进货
public synchronized void get() {
if (product >= 10) {
System.out.println("产品已满!");
} else {
System.out.println(Thread.currentThread().getName() + " : " + ++product);
}
}
//卖货
public synchronized void sale() {
if (product <= 0) {
System.out.println("没货了!");
} else {
System.out.println(Thread.currentThread().getName() + " : " + --product);
}
}
}
//生产者
class Productor implements Runnable {
private Clerk clerk ;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0 ; i < 20 ; i++) {
clerk.get();
}
}
}
//消费者
class Consumer implements Runnable {
private Clerk clerk ;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0 ; i < 20 ; i++) {
clerk.sale();
}
}
}
以上案例 输出结果。
...
生产者A : 9
生产者A : 10
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
消费者B : 9
消费者B : 8
消费者B : 7
消费者B : 6
消费者B : 5
消费者B : 4
消费者B : 3
消费者B : 2
消费者B : 1
消费者B : 0
没货了!
没货了!
没货了!
没货了!
没货了!
没货了!
...
2. 等待唤醒机制
2.1 解决以上问题
我们可以通过等待唤醒机制,解决上面问题。我们使用线程同步技术,只需要将店员的功能修改成如下便可以达到目的。
//店员
class Clerk {
private int product = 0 ;
//进货
public synchronized void get() {
if (product >= 10) {
System.out.println("产品已满!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
}
//卖货
public synchronized void sale() {
if (product <= 0) {
System.out.println("没货了!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
}
}
输出结果
生产者A : 8
生产者A : 9
生产者A : 10
产品已满!
消费者B : 9
消费者B : 8
消费者B : 7
消费者B : 6
消费者B : 5
消费者B : 4
消费者B : 3
消费者B : 2
消费者B : 1
消费者B : 0
没货了!
生产者A : 1
生产者A : 2
生产者A : 3
生产者A : 4
2.2 以上存在的问题
我们分析,上面案例其实是存在问题的,我们做些修改,把这个问题暴露出来。
修改如下:
– 生产者库存改为1
– 生产时增加0.2秒的延迟
//店员
class Clerk {
private int product = 0 ;
//进货
public synchronized void get() {
//修改库存为1
if (product >= 1) {
System.out.println("产品已满!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
}
//卖货
public synchronized void sale() {
if (product <= 0) {
System.out.println("没货了!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
}
}
//生产者
class Productor implements Runnable {
private Clerk clerk ;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0 ; i < 20 ; i++) {
try {
//给店员处理增加0.2秒的延迟。
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.get();
}
}
}
运行结果如下:
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
生产者A : 1
产品已满!
分析:
同时,我们发现程序并没有停止 !!!
生产者延迟了0.2秒,所以消费者是更快的。
问题出在哪?
我们现在是有20次生产的机会,假设只给二次机会生产者,同时只给消费者一次机会。
因为生产者sleep了,所以消费者先获取的锁,消费者获取锁之后,判断product < 0 进入wait()释放锁,生产者获得锁,进行生产,
此时product = 1,并通知消费者线程,消费者线程重新获得锁,直接从wait()代码开始执行,其实什么也没干,消费者就结束了,那么
生产者又进入第二次生产,发现库存满了,然后生产者进入等待。然后我们分析当前状态,消费者线程停了,并且一次都没有消费,库存
里仍然有1个,而且生产者认为库存满了,一直在等待消费线程通知。也就是说消费者,需要做二次循环才能消费一个资源。那么扩大到
20次循环,生产者只生产了10次,消费者就已经结束了。。上面输出结果,数一下其实就是大概这样,生产者阻塞在了第11次循环。
可以想个实际的例子,你去买烟,店员说柜台没货了,然后让你在原地等待,他打电话让人去仓库送货过来,送货人员货到了通知给店员,
店员居然不给你烟,直接让你走,请问你走不走啊?白来一趟吗
2.3 以上存在的问题解决方案
我们发现,消费者wait()中唤醒后,其实什么都没有做,因为下面的真正消费的代码是else了,但实际既然发起了一次消费,即使没货
消费者做了等待,就应该在等待之后做一次真正的消费,也就是和else里的工作一样。
所以我们做如下修改,去掉else,把里面代码放到外面执行就没有问题了
//店员
class Clerk {
private int product = 0 ;
//进货
public synchronized void get() {
//修改库存为1
if (product >= 1) {
System.out.println("产品已满!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//去掉else
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
//卖货
public synchronized void sale() {
if (product <= 0) {
System.out.println("没货了!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//去掉else
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
}
最后整个程序就能正常结束了。
2.4 多个生产者和多个消费者产生虚假唤醒
上面生产者和消费者各一个时,确实没问题了,下面我们看看多个生产者和多个消费者的情况是怎样的。
new Thread(pro, "生产者A").start();
new Thread(cus, "消费者B").start();
new Thread(pro, "生产者C").start();
new Thread(cus, "消费者D").start();
结果如下:
生产者A : -23
生产者C : -22
生产者A : -21
生产者C : -20
生产者A : -19
生产者C : -18
生产者A : -17
生产者C : -16
生产者A : -15
生产者C : -14
生产者A : -13
生产者C : -12
生产者A : -11
生产者C : -10
生产者A : -9
生产者C : -8
生产者A : -7
生产者C : -6
生产者A : -5
生产者C : -4
生产者A : -3
生产者C : -2
生产者A : -1
生产者C : 0
结果有很多负数,什么原因呢???
假设生产者通知获取资源,第一个消费者获取到锁,发现没库存,进入等待,释放锁,结果被第二个消费
者抢到锁,第二个消费者发现没有库存,又释放锁。之后有两种可能,一种是又被第一个消费者获取到锁,
执行库存扣减,变成负数了;另一种可能被一个生产者获取到锁,库存做了一次加法,释放锁,后面至少
有一个消费者能正常消费。现在问题就出现在第一种情况。。消费者不停被唤醒,这就是是虚假的唤醒。
参考链接:http://blog.csdn.net/luckybug007/article/details/70053669
2.5 消除虚假唤醒
wait()方法注释有虚假唤醒的解决方法,就是把if换成while,在被唤醒后重新做一次判断,看是否是真的
满足唤醒自己的条件达到。
于是修改代码:
//店员
class Clerk {
private int product = 0 ;
//进货
public synchronized void get() {
//修改库存为1
while (product >= 1) {
System.out.println("产品已满!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//去掉else
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
//卖货
public synchronized void sale() {
while (product <= 0) {
System.out.println("没货了!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//去掉else
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
}
结果,一切正常:
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
生产者A : 1
消费者B : 0
2.6 另外:
线程在获取synchronized锁时,状态为blocked状态,在获取到synchronized锁内,主动通过wait释放锁,状态为waiting状态,
这两种等待队列不是同一个队列。所以上面调用notify,目的是为了唤醒waiting状态线程,但是同时notify后也释放了synchronized
锁,blocked的队列也有机会获取锁。
为了避免虚假唤醒,应该总是使用循环。