一、問題描述
在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。如何用代碼描述此類問題。
二、一個消費者線程、一個生產者線程
有幾點需要強調:
1.main方法中的資源res的線程t1、t2共享的。所以當操作該資源時需要同步
2.同步函數的鎖即爲this,所以res即爲線程t1、t2的鎖
3.因爲只有兩個線程所以能夠準確的喚醒對象線程(即消費者線程喚醒生產者進程、生產者進程喚醒消費者進程),
那麼問題就來了,如果是多消費者進程,多生產進程怎麼辦?
package com.thread.pcprob;
/**
* 共享數據
* @author dqf
*
*/
public class Resource {
//產品編號
private Integer proNo = 0;
//判斷是消費數據,還是生產數據
private boolean flag = false;
//生產數據
public synchronized void produce(){
if(flag){//如果flag=true,則等待消費;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產-->" + (++proNo));
flag = true;
this.notify();//喚醒等待進程
}
//消費數據
public synchronized void consume(){
if(!flag){//如果flag=false,則等待生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費-------->" + proNo);
flag = false;
this.notify();//喚醒等待進程
}
}
package com.thread.pcprob;
/**
* 生產者
* @author dqf
*
*/
public class Producer implements Runnable{
private Resource res;
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
while(true){
res.produce();
}
}
}
package com.thread.pcprob;
/**
* 消費者
* @author dqf
*
*/
public class Consumer implements Runnable{
private Resource res;
public Consumer(Resource res) {
this.res = res;
}
@Override
public void run() {
while(true){
res.consume();
}
}
}
package com.thread.pcprob;
public class App {
public static void main(String[] args) {
Resource res = new Resource();
Producer p = new Producer(res);
Consumer c = new Consumer(res);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
部分輸出:
消費-------->159091
生產-->159092
消費-------->159092
生產-->159093
消費-------->159093
生產-->159094
消費-------->159094
生產-->159095
消費-------->159095
生產-->159096
消費-------->159096
生產-->159097
消費-------->159097
生產-->159098
消費-------->159098
生產-->159099
消費-------->159099
三、多生產者進程、多消費者進程
我們把main方法修改如下:
public class App {
public static void main(String[] args) {
Resource res = new Resource();
Producer p = new Producer(res);
Consumer c = new Consumer(res);
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
部分輸出:
消費-------->123384
消費-------->123384
生產-->123385
消費-------->123385
消費-------->123385
生產-->123386
消費-------->123386
消費-------->123386
生產-->123387
消費-------->123387
消費-------->123387
生產-->123388
消費-------->123388
我們發現會出現兩種錯誤的情況
1.生產一個,消費多次
2.生產多次,消費一次
下面來分析產生兩種問題的原因:
假設一種狀態:
flag = false,t1獲取到鎖,t2、t3、t4等待鎖釋放。
因爲flag爲false,t1阻塞自動釋放鎖,t2獲取鎖,同樣因爲flag爲false,t2阻塞自動釋放鎖。
若t3獲取鎖,執行到this.notify(),則會喚醒一個阻塞狀態的線程,如果喚醒了t1線程,t1線程結束後又喚醒了t2。
則,就會出現多次生產,一次消費的情況。
其實造成這種情況的主要原因是:不能指定具體喚醒哪一個線程、或者說不能指定喚醒哪一類線程。導致,生產進程結束後,又喚醒了生產進程;或者消費進程結束後,又喚醒消費進程。
如果將Resource改爲(if改爲while),那麼在線程被喚醒後,都會再次判斷一次flag。
package com.thread.pcprob;
/**
* 共享數據
* @author dqf
*
*/
public class Resource {
//產品編號
private Integer proNo = 0;
//判斷是消費數據,還是生產數據
private boolean flag = false;
//生產數據
public synchronized void produce(){
while(flag){//如果flag=true,則等待消費;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產-->" + (++proNo));
flag = true;
this.notify();//喚醒等待進程
}
//消費數據
public synchronized void consume(){
while(!flag){//如果flag=false,則等待生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費-------->" + proNo);
flag = false;
this.notify();//喚醒等待進程
}
}
但是這種情況又會導致,t1、t2、t3、t4全部處於阻塞狀態。
最終寫法(notify改爲notifyAll):
package com.thread.pcprob;
/**
* 共享數據
* @author dqf
*
*/
public class Resource {
//產品編號
private Integer proNo = 0;
//判斷是消費數據,還是生產數據
private boolean flag = false;
//生產數據
public synchronized void produce(){
while(flag){//如果flag=true,則等待消費;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產-->" + (++proNo));
flag = true;
this.notifyAll();//喚醒等待進程
}
//消費數據
public synchronized void consume(){
while(!flag){//如果flag=false,則等待生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費-------->" + proNo);
flag = false;
this.notifyAll();//喚醒等待進程
}
}