上一篇講述了線程的互斥(同步),但是在很多情況下,僅僅同步是不夠的,還需要線程與線程協作(通信),生產者/消費者問題是一個經典的線程同步以及通信的案例。該問題描述了兩個共享固定大小緩沖區的線程,即所謂的“生產者”和“消費者”在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩沖區中,然後重復此過程。與此同時,消費者也在緩沖區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩沖區滿時加入數據,消費者也不會在緩沖區中空時消耗數據。要解決該問題,就必須讓生產者在緩沖區滿時休眠(要麼干脆就放棄數據),等到下次消費者消耗緩沖區中的數據的時候,生產者才能被喚醒,開始往緩沖區添加數據。同樣,也可以讓消費者在緩沖區空時進入休眠,等到生產者往緩沖區添加數據之後,再喚醒消費者,通常采用線程間通信的方法解決該問題。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。本文講述了JDK5之前傳統線程的通信方式,更高級的通信方式可參見Java線程(九):Condition-線程通信更高效的方式和Java線程(篇外篇):阻塞隊列BlockingQueue。
假設有這樣一種情況,有一個盤子,盤子裡只能放一個雞蛋,A線程專門往盤子裡放雞蛋,如果盤子裡有雞蛋,則一直等到盤子裡沒雞蛋,B線程專門從盤子裡取雞蛋,如果盤子裡沒雞蛋,則一直等到盤子裡有雞蛋。這裡盤子是一個互斥區,每次放雞蛋是互斥的,每次取雞蛋也是互斥的,A線程放雞蛋,如果這時B線程要取雞蛋,由於A沒有釋放鎖,B線程處於等待狀態,進入阻塞隊列,放雞蛋之後,要通知B線程取雞蛋,B線程進入就緒隊列,反過來,B線程取雞蛋,如果A線程要放雞蛋,由於B線程沒有釋放鎖,A線程處於等待狀態,進入阻塞隊列,取雞蛋之後,要通知A線程放雞蛋,A線程進入就緒隊列。我們希望當盤子裡有雞蛋時,A線程阻塞,B線程就緒,盤子裡沒雞蛋時,A線程就緒,B線程阻塞,代碼如下:
[java] view plaincopyprint?在CODE上查看代碼片派生到我的代碼片
import java.util.ArrayList;
import java.util.List;
/** 定義一個盤子類,可以放雞蛋和取雞蛋 */
public class Plate {
/** 裝雞蛋的盤子 */
List<Object> eggs = new ArrayList<Object>();
/** 取雞蛋 */
public synchronized Object getEgg() {
while (eggs.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object egg = eggs.get(0);
eggs.clear();// 清空盤子
notify();// 喚醒阻塞隊列的某線程到就緒隊列
System.out.println("拿到雞蛋");
return egg;
}
/** 放雞蛋 */
public synchronized void putEgg(Object egg) {
while (eggs.size() > 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
eggs.add(egg);// 往盤子裡放雞蛋
notify();// 喚醒阻塞隊列的某線程到就緒隊列
System.out.println("放入雞蛋");
}
static class AddThread implements Runnable {
private Plate plate;
private Object egg = new Object();
public AddThread(Plate plate) {
this.plate = plate;
}
public void run() {
plate.putEgg(egg);
}
}
static class GetThread implements Runnable {
private Plate plate;
public GetThread(Plate plate) {
this.plate = plate;
}
public void run() {
plate.getEgg();
}
}
public static void main(String args[]) {
Plate plate = new Plate();
for(int i = 0; i < 10; i++) {
new Thread(new AddThread(plate)).start();
new Thread(new GetThread(plate)).start();
}
}
}
輸出結果:
[java] view plaincopyprint?在CODE上查看代碼片派生到我的代碼片
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
程序開始,A線程判斷盤子是否為空,放入一個雞蛋,並且喚醒在阻塞隊列的一個線程,阻塞隊列為空;假設CPU又調度了一個A線程,盤子非空,執行等待,這個A線程進入阻塞隊列;然後一個B線程執行,盤子非空,取走雞蛋,並喚醒阻塞隊列的A線程,A線程進入就緒隊列,此時就緒隊列就一個A線程,馬上執行,放入雞蛋;如果再來A線程重復第一步,在來B線程重復第二步,整個過程就是生產者(A線程)生產雞蛋,消費者(B線程)消費雞蛋。
前段時間看了張孝祥老師線程的視頻,講述了一個其學員的面試題,也是線程通信的,在此也分享一下。
題目:子線程循環10次,主線程循環100次,如此循環100次,好像是空中網的筆試題。
[java] view plaincopyprint?在CODE上查看代碼片派生到我的代碼片
public class ThreadTest2 {
public static void main(String[] args) {
final Business business = new Business();
new Thread(new Runnable() {
@Override
public void run() {
threadExecute(business, "sub");
}
}).start();
threadExecute(business, "main");
}
public static void threadExecute(Business business, String threadType) {
for(int i = 0; i < 100; i++) {
try {
if("main".equals(threadType)) {
business.main(i);
} else {
business.sub(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Business {
private boolean bool = true;
public synchronized void main(int loop) throws InterruptedException {
while(bool) {
this.wait();
}
for(int i = 0; i < 100; i++) {
System.out.println("main thread seq of " + i + ", loop of " + loop);
}
bool = true;
this.notify();
}
public synchronized void sub(int loop) throws InterruptedException {
while(!bool) {
this.wait();
}
for(int i = 0; i < 10; i++) {
System.out.println("sub thread seq of " + i + ", loop of " + loop);
}
bool = false;
this.notify();
}
}