并發編程這方面以前關注得比較少,惡補一下,推薦一個好的網站:并發編程網 - ifeve.com,上面全是各種大牛原創或編譯的并發編程文章。
今天先來學習Semaphore(信號量),字面上看,根本不知道這東西是干啥的,借用 并發工具類(三)控制并發線程數的Semaphore一文中的交通紅綠信號燈的例子來理解一下:
一條4車道的主干道,假設100米長,每輛車假設占用的長度為10米(考慮到前后車距),也就是說這條道上滿負載運行的話,最多只能容納4*(100/10)=40輛車,如果有120輛車要通過的話(為簡單起見,一波40輛,分成3波),就必須要紅綠信號燈來調度了,對于最前面的一波來講,它們看到的是綠燈,允許通過,第一波全進入道路后,紅綠燈變成紅色,表示后面的2波,要停下來等候第1波車輛全通過,然后紅綠燈才會變成綠色,讓第2波通過,如此運轉下去....
這跟多線程并發有啥關系呢?Semaphore就是紅綠信號燈,3波車輛就是3個并發的線程,而主干道就是多個線程要并發訪問的公用資源,由于資源有限,所以必須通過Semaphore來控制線程對資源的訪問,否則就變成資源競爭,嚴重的話會導致死鎖等問題。
下面用一個示例演示,假設有N個并發線程都要打印文件,但是打印機只有1臺,先來一個打印隊列類:
package yjmyzz.lesson01; 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)
package yjmyzz.lesson01; 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()); } }
好了,測試一把:
package yjmyzz.lesson01; 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的部分,變成下面這樣:
package yjmyzz.lesson01; 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個人都看中了同一個箱子,都要用手中的鑰匙去搶著開箱)。
所以得改進一下:
package yjmyzz.lesson02; 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上了,出現了多個線程同時搶一個資源的情況。
參考文章:
http://ifeve.com/thread-synchronization-utilities-2/
文章列表