java 併發之 Semphore
簡介
Semaphore(訊號量)是用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。很多年以來,我都覺得從字面上很難理解Semaphore所表達的含義,只能把它比作是控制流量的紅綠燈,比如XX馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入XX馬路,但是如果前一百輛中有五輛車已經離開了XX馬路,那麼後面就允許有5輛車駛入馬路,這個例子裡說的車就是執行緒,駛入馬路就表示執行緒在執行,離開馬路就表示執行緒執行完成,看見紅燈就表示執行緒被阻塞,不能執行。
示例
下面用一個示例演示,假設有N個併發執行緒都要列印檔案,但是印表機只有1臺,先來一個列印佇列類:
import java.util.concurrent.Semaphore;
public class PrintQueue {
private final Semaphore semaphore;
public PrintQueue() {
semaphore = new Semaphore(1);//限定了共享資源只能有1個(相當於只有一把鑰匙)
}
public void printJob(Object document) {
try {
semaphore.acquire();//取得對共享資源的訪問權(即拿到了鑰匙))
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//鑰匙用完了,要還回去,這樣其它執行緒才能繼續有序的拿到鑰匙,訪問資源
}
}
}
由於是在多執行緒環境中,真正執行的作業處理,得繼承自Runnable(或Callable)
public class Job implements Runnable {
private PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
public void run() {
System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
}
好了,測試一把:
public class Main {
public static void main(String args[]) {
PrintQueue printQueue = new PrintQueue();
int threadCount = 3;
Thread thread[] = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
thread[i] = new Thread(new Job(printQueue), "Thread" + i);
}
for (int i = 0; i < threadCount; i++) {
thread[i].start();
}
}
}
輸出:
Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job during 7 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job during 5 seconds
Thread2: The document has been printed
Thread1: PrintQueue: Printing a Job during 1 seconds
Thread1: The document has been printed
從輸出上看,執行緒0列印完成後,執行緒2才開始列印,然後才是執行緒1,沒有出現一哄而上,搶佔印表機的情況。這樣可能沒啥感覺,我們把PrintQueue如果去掉Semaphore的部分,變成下面這樣:
public class PrintQueue {
//private final Semaphore semaphore;
public PrintQueue() {
//semaphore = new Semaphore(1);//限定了共享資源只能有1個(相當於只有一把鑰匙)
}
public void printJob(Object document) {
try {
//semaphore.acquire();//取得對共享資源的訪問權(即拿到了鑰匙))
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//semaphore.release();//鑰匙用完了,要還回去,這樣其它執行緒才能繼續有序的拿到鑰匙,訪問資源
}
}
}
這回的輸出:
Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread2: PrintQueue: Printing a Job during 4 seconds
Thread1: PrintQueue: Printing a Job during 8 seconds
Thread0: PrintQueue: Printing a Job during 1 seconds
Thread0: The document has been printed
Thread2: The document has been printed
Thread1: The document has been printed
可以發現,3個執行緒全都一擁而上,同時開始列印,也不管印表機是否空閒,實際應用中,這樣必然出問題。
好的,繼續,突然有一天,公司有錢了,又買了2臺印表機,這樣就有3臺印表機了,這時候怎麼辦呢?簡單的把PrintQueue構造器中的
public PrintQueue() {
semaphore = new Semaphore(3);
}
就行了嗎?仔細想想,就會發現問題,程式碼中並沒有哪裡能告訴執行緒哪個印表機正在列印,哪個印表機當前空閒,所以仍然有可能出現N個執行緒(N<=3)同時搶一臺印表機的情況(即:如果把控制權當成鑰匙的話,相當於有可能3個人各領取到了1把鑰匙,但是這3把鑰匙是相同的,3個人都看中了同一個箱子,都要用手中的鑰匙去搶著開箱)。
所以得改進一下:
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrintQueue {
private boolean freePrinters[];//用來存放印表機的狀態,true表示空閒,false表示正在列印
private Lock lockPrinters;//增加了鎖,保證多個執行緒,只能獲取得鎖,才能查詢哪臺印表機空閒的
private final Semaphore semaphore;
public PrintQueue() {
int printerNum = 3;//假設有3臺印表機
semaphore = new Semaphore(printerNum);
freePrinters = new boolean[printerNum];
for (int i = 0; i < printerNum; i++) {
freePrinters[i] = true;//初始化時,預設所有印表機都空閒
}
lockPrinters = new ReentrantLock();
}
private int getPrinter() {
int ret = -1;
try {
lockPrinters.lock();//先加鎖,保證1次只能有1個執行緒來獲取空閒的印表機
for (int i = 0; i < freePrinters.length; i++) {
//遍歷所有印表機的狀態,發現有第1個空閒的印表機後,領取號碼,
// 並設定該印表機為繁忙狀態(因為馬上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最後別忘記了解鎖,這樣後面的執行緒才能上來領號
lockPrinters.unlock();
}
return ret;
}
public void printJob(Object document) {
try {
semaphore.acquire();
int assignedPrinter = getPrinter();//領號
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n", Thread.currentThread().getName(),
assignedPrinter, duration);
Thread.sleep(duration);
freePrinters[assignedPrinter] = true;//列印完以後,將該印表機重新恢復為空閒狀態
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
測試一下,這回把執行緒數增加到5,輸出結果類似下面這樣:
Thread0: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread2: The document has been printed
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread3: The document has been printed
Thread1: The document has been printed
從輸出結果可以看出,一次最多隻能有3個執行緒使用這3臺印表機,而且每個執行緒使用的印表機互不衝突,列印完成後,空閒的印表機會給其它執行緒繼續使用,繼續折騰,如果把getPrinter()中加鎖的部分去掉,即:
private int getPrinter() {
int ret = -1;
try {
//lockPrinters.lock();//先加鎖,保證1次只能有1個執行緒來獲取空閒的印表機
for (int i = 0; i < freePrinters.length; i++) {
//遍歷所有印表機的狀態,發現有第1個空閒的印表機後,領取號碼,
// 並設定該印表機為繁忙狀態(因為馬上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最後別忘記了解鎖,這樣後面的執行緒才能上來領號
//lockPrinters.unlock();
}
return ret;
}
再跑一下,結果如何,為了放大沖突,這回開到15個執行緒來搶3臺印表機,輸出如下:
Thread0: Going to print a job
Thread14: Going to print a job
Thread13: Going to print a job
Thread12: Going to print a job
Thread11: Going to print a job
Thread10: Going to print a job
Thread9: Going to print a job
Thread8: Going to print a job
Thread7: Going to print a job
Thread6: Going to print a job
Thread5: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
Thread0: The document has been printed
Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
Thread13: The document has been printed
Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
Thread11: The document has been printed
Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
Thread14: The document has been printed
Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread12: The document has been printed
Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
Thread10: The document has been printed
Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
Thread8: The document has been printed
Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
Thread7: The document has been printed
Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
Thread9: The document has been printed
Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
Thread5: The document has been printed
Thread6: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
Thread1: The document has been printed
Thread3: The document has been printed
Thread2: The document has been printed
注意標粗的部分:Thread0與Thread14同時分配到了Printer0上了,出現了多個執行緒同時搶一個資源的情況。