Java基礎知識筆記-14-併發html
讀者可能已經很熟悉操做系統中的多任務(multitasking): 在同一刻運行多個程序的能力。 例如,在編輯或下載郵件的同時能夠打印文件。今天,人們極可能有單臺擁有多個CPU的計算機,可是,併發執行的進程數目並非由CPU數目制約的。操做系統將CPU的時間片分配給每個進程,給人並行處理的感受。java
多線程程序在較低的層次上擴展了多任務的概念:一個程序同時執行多個任務。一般,每個任務稱爲一個線程(thread), 它是線程控制的簡稱。能夠同時運行一個以上線程的程序稱爲多線程程序(multithreaded)。程序員
那麼,多進程與多線程有哪些區別呢? 本質的區別在於每一個進程擁有本身的一整套變量,而線程則共享數據。這聽起來彷佛有些風險,的確也是這樣,在本章稍後將能夠看到這個問題。然而,共享變量使線程之間的通訊比進程之間的通訊更有效、更容易。此外,在有些操做系統中,與進程相比較,線程更「 輕量級」,建立、撤銷一個線程比啓動新進程的開銷要小得多。web
在實際應用中,多線程很是有用。例如,一個瀏覽器能夠同時下載幾幅圖片。一個Web服務器須要同時處理幾個併發的請求。圖形用戶界面(GUI)程序用一個獨立的線程從宿主操做環境中收集用戶界面的事件。本章將介紹如何爲Java應用程序添加多線程能力。數據庫
這裏從察看一個沒有使用多線程的程序開始。用戶很難讓它執行多個任務。在對其進行剖析以後,將展現讓這個程序運行幾個彼此獨立的多個線程是很容易的。這個程序採用不斷地移動位置的方式實現球跳動的動畫效果,若是發現球碰到牆壁,將進行重繪。編程
當點擊Start按鈕時,程序將從屏幕的左上角彈出一個球,這個球便開始彈跳。Start按鈕的處理程序將調用addBall方法。這個方法循環運行1000次move。每調用一次move, 球就會移動一點,當碰到牆壁時,球將調整方向,並從新繪製面板。數組
Ball ball = new Ball(); panel.add(ball); for (int i = 1 ;i <= STEPS;i++) { ball.move(panel.getBounds()); panel.paint(panel.getCraphics()); Thread.sleep(DELAY); }
調用Threadsleep不會建立一個新線程,sleep是Thread類的靜態方法,用於暫停當前線程的活動。sleep方法能夠拋出一個InterruptedException異常。稍後將討論這個異常以及對它的處理。如今,只是在發生異常時簡單地終止彈跳。若是運行這個程序,球就會自如地來回彈跳,可是,這個程序徹底控制了整個應用程序。若是你在球完成1000次彈跳以前已經感到厭倦了,並點擊Close按鈕會發現球仍然還在彈跳。在球本身結束彈跳以前沒法與程序進行交互。瀏覽器
能夠將移動球的代碼放置在一個獨立的線程中,運行這段代碼能夠提升彈跳球的響應能力。實際上,能夠發起多個球,每一個球都在本身的線程中運行。另外,AWT的事件分派線程(event dispatch thread)將一直地並行運行,以處理用戶界面的事件。因爲每一個線程都有機會得以運行,因此在球彈跳期間,當用戶點擊Close按鈕時,事件調度線程將有機會關注到這個事件,並處理「關閉」這一動做。緩存
這裏用球彈跳代碼做爲示例,讓你們對併發處理有一個視覺印象。一般,人們總會提防長時間的計算。這個計算極可能是某個大框架的一個組成部分,例如,GUI或web框架。不管什麼時候框架調用自身的方法都會很快地返回一個異常。若是須要執行一個比較耗時的任務,應當併發地運行任務。安全
下面是在一個單獨的線程中執行一個任務的簡單過程:
public interface Runnable { void run(); }
因爲Runnable是一個函數式接口,能夠用lambda表達式創建一個實例:
Runnable r = () -> { taskcode };
Thread t = new Thread(r);
t.start()
;Runnable r = () -> { try { for (int i = 1 ; i <=: STEPS; i++) { ball.move(comp.getBounds()); comp.repaint(); Thread.sleep(DELAY); } } catch (InterruptedException e) { } }; Thread t = new Thread(r); t.start();
一樣地,須要捕獲sleep方法可能拋出的異常InterruptedException。下一節將討論這個異常。在通常狀況下,線程在中斷時被終止。所以,當發生InterruptedException異常時,run方法將結束執行。
不管什麼時候點擊Start按鈕,球會移入一個新線程。僅此而已!如今應該知道如何並行運行多個任務了。本章其他部分將闡述如何控制線程之間的交互。
Runnable對象僅僅做爲Thread對象的target,Runable實現類裏包含的run()方法僅做爲線程執行體。而實際的線程對象仍然是Thread實例,只是該Thread線程負責執行其target的run()方法。
也能夠經過構建一個Thread類的子類定義一個線程,以下所示
- 定義Thread類的子類 ,並重寫該類的run()方法,該run()方法的方法體就體現了線程須要完成的任務。所以把run()方法稱爲線程執行體.。
- 建立Thread子類的實例,即建立了線程對象。
- 調用線程對象的start()方法來啓動該線程。
class MyThread extends Thread { public void run() { taskcode } }而後,構造一個子類的對象,並調用start方法。不過,這種方法已再也不推薦。應該將要並行運行的任務與運行機制解耦合。若是有不少任務,要爲每一個任務建立一個獨立的線程所付出的代價太大了。可使用線程池來解決這個問題,有關內容請參看第14.9節。
警告:不要調用Thread類或Runnable對象的run方法。直接調用run方法,只會執行同一個線程中的任務,而不會啓動新線程。應該調用
Thread.start
方法。這個方法將建立一個執行run方法的新線程。
當線程的run方法執行方法體中最後一條語句後,並經由執行return語句返冋時,或者出現了在方法中沒有捕獲的異常時,線程將終止。在Java的早期版本中,還有一個stop方法,其餘線程能夠調用它終止線程。可是,這個方法如今已經被棄用了。
沒有能夠強制線程終止的方法。然而,interrupt方法能夠用來請求終止線程。
當對一個線程調用interrupt方法時,線程的中斷狀態將被置位。這是每個線程都具備的boolean標誌。每一個線程都應該不時地檢査這個標誌,以判斷線程是否被中斷。
要想弄清中斷狀態是否被置位,首先調用靜態的Thread.currentThread方法得到當前線程,而後調用islnterrupted方法:
while (!Thread.currentThread().islnterrupted() && more work todo) { domorework }
可是,若是線程被阻塞,就沒法檢測中斷狀態。這是產生InterruptedException異常的地方。當在一個被阻塞的線程(調用sleep或wait)上調用interrupt方法時,阻塞調用將會被InterruptedException異常中斷。(存在不能被中斷的阻塞I/O調用,應該考慮選擇可中斷的調用。有關細節請參看卷2的第1章和第3章。)
沒有任何語言方面的需求要求一個被中斷的線程應該終止。中斷一個線程不過是引發它的注意。被中斷的線程能夠決定如何響應中斷。某些線程是如此重要以致於應該處理完異常後,繼續執行,而不理會中斷。可是,更廣泛的狀況是,線程將簡單地將中斷做爲一個終止的請求。這種線程的run方法具備以下形式:
Runnable r = () -> { try { while (!Thread.currentThread().islnterrupted0 && more work todo) { do morework } } catch(InterruptedException e) { // thread was interrupted during sleep or wait } finally { cleanup,if required } // exiting the run method terminates the thread };
若是在每次工做迭代以後都調用sleep方法(或者其餘的可中斷方法),islnterrupted檢測既沒有必要也沒有用處。若是在中斷狀態被置位時調用sleep方法,它不會休眠。相反,它將清除這一狀態(!)並拋出InterruptedException。所以,若是你的循環調用sleep,不會檢測中斷狀態。相反,要以下所示捕獲 InterruptedException異常:
Runnable r = () -> { try { while (!Thread.currentThread().isInterrupter() && more work todo) { do morework Tread.sleep(delay); } } catch(InterruptedException e) { // thread was interrupted during sleep } finally{ cleanup,if required } // exiting the run method terminates the thread };
註釋:有兩個很是相似的方法,interrupted和islnterrupted。Interrupted方法是一個靜態方法,它檢測當前的線程是否被中斷。並且,調用interrupted方法會清除該線程的中斷狀態。另外一方面,islnterrupted方法是一個實例方法,可用來檢驗是否有線程被中斷。調用這個方法不會改變中斷狀態。
在不少發佈的代碼中會發現InterruptedException異常被抑制在很低的層次上,像這樣:
void mySubTask() { ... try { sleep(delay); } catch (InterruptedException e) {} // Don't ignore! ... }
不要這樣作!若是不認爲在catch子句中作這一處理有什麼好處的話,仍然有兩種合理的選擇:
Thread.currentThread().interrupt()
來設置中斷狀態。因而,調用者能夠對其進行檢測。void mySubTask() { try { sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
void mySubTask() throws InterruptedException { ... sleep(delay); ... }
java.Iang.Thread1.0
void interrupts(); //向線程發送中斷請求。線程的中斷狀態將被設置爲true。若是目前該線程被一個sleep調用阻塞,那麼,InterruptedException異常被拋出。 static boolean interrupted(); //測試當前線程(即正在執行這一命令的線程)是否被中斷。注意,這是一個靜態方法。這一調用會產生反作用---它將當前線程的中斷狀態重置爲false。 boolean islnterrupted(); //測試線程是否被終止。不像靜態的中斷方法,這一調用不改變線程的中斷狀態。 static Thread currentThread(); //返回表明當前執行線程的Thread對象。
線程能夠有以下6種狀態:
下一節對每一種狀態進行解釋。要肯定一個線程的當前狀態,可調用getState方法。
當用new操做符建立一個新線程時,如newThread(r),該線程尚未開始運行。這意味着它的狀態是new。當一個線程處於新建立狀態時,程序尚未開始運行線程中的代碼。在線程運行以前還有一些基礎工做要作。
一旦調用start方法,線程處於runnable狀態。一個可運行的線桿可能正在運行也可能沒有運行,這取決於操做系統給線程提供運行的時間。(Java的規範說明沒有將它做爲一個單獨狀態。一個正在運行中的線程仍然處於可運行狀態。)
一旦一個線程開始運行,它沒必要始終保持運行。事實上,運行中的線程被中斷,目的是爲了讓其餘線程得到運行機會。線程調度的細節依賴於操做系統提供的服務。搶佔式調度系統給每個可運行線程一個時間片來執行任務。當時間片用完,操做系統剝奪該線程的運行權,並給另外一個線程運行機會(見圖14-4 )。當選擇下一個線程時,操做系統考慮線程的優先級---更多的內容見第4.1節。
如今全部的桌面以及服務器操做系統都使用搶佔式調度。可是,像手機這樣的小型設備可能使用協做式調度。在這樣的設備中,一個線程只有在調用yield方法、或者被阻塞或等待時,線程才失去控制權。
在具備多個處理器的機器上,每個處理器運行一個線程,能夠有多個線程並行運行。固然,若是線程的數目多於處理器的數目,調度器依然採用時間片機制。
記住,在任何給定時刻,二個可運行的線程可能正在運行也可能沒有運行(這就是爲何將這個狀態稱爲可運行而不是運行)。
當線程處於被阻塞或等待狀態時,它暫時不活動。它不運行任何代碼且消耗最少的資源。直到線程調度器從新激活它。細節取決於它是怎樣達到非活動狀態的。
當一個線程試圖獲取一個內部的對象鎖(而不是javiutiUoncurrent庫中的鎖),而該鎖被其餘線程持有,則該線程進入阻塞狀態(咱們在14.5.3節討論java.util.concurrent鎖,在14.5.5節討論內部對象鎖)。當全部其餘線程釋放該鎖,而且線程調度器容許本線程持有它的時候,該線程將變成非阻塞狀態。
當線程等待另外一個線程通知調度器一個條件時,它本身進入等待狀態。咱們在第14.5.4節來討論條件。在調用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時,就會出現這種狀況。實際上,被阻塞狀態與等待狀態是有很大不一樣的。
有幾個方法有一個超時參數。調用它們致使線程進入計時等待(timed waiting) 狀態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有Thread.sleep和Object.wait、Thread.join、Lock.tryLock以及Condition.await的計時版。
圖14-3展現了線程能夠具備的狀態以及從一個狀態到另外一個狀態可能的轉換。當一個線程被阻塞或等待時(或終止時),另外一個線程被調度爲運行狀態。當一個線程被從新激活(例如,由於超時期滿或成功地得到了一個鎖),調度器檢查它是否具備比當前運行線程更高的優先級。若是是這樣,調度器從當前運行線程中挑選一個,剝奪其運行權,選擇一個新的線程運行。
線程因以下兩個緣由之一而被終止:
注意:當主線程結束時,其餘線程不受任何影響,能夠調用線程對象的isAlive()方法,當線程處於就緒,運行,阻塞三種狀態時,該方法將返回true,當線程處於新建,死亡兩種狀態時,該方法將返回false
不要試圖對一個已近死亡的線程調用start()方法使它從新啓動。會拋出異常
在Java程序設計語言中,每個線程有一個優先級。默認狀況下,一個線程繼承它的父線程的優先級。能夠用setPriority方法提升或下降任何一個線程的優先級。能夠將優先級設 置爲在MIN_PRIORITY(在Thread類中定義爲1)與MAX_PRIORITY(定義爲10)之間的任何值。NORM_PRIORITY被定義爲5。
每當線程調度器有機會選擇新線程時,它首先選擇具備較高優先級的線程。可是,線程優先級是高度依賴於系統的。當虛擬機依賴於宿主機平臺的線程實現機制時,Java線程的優先級被映射到宿主機平臺的優先級上,優先級個數也許更多,也許更少。
例如,Windows有7個優先級別。一些Java優先級將映射到相同的操做系統優先級。在Oracle爲Linux提供的Java虛擬機中,線程的優先級被忽略一全部線程具備相同的優先級。初級程序員經常過分使用線程優先級。爲優先級而煩惱是事出有因的。不要將程序構建爲功能的正確性依賴於優先級。
警告:若是確實要使用優先級,應該避免初學者常犯的一個錯誤。若是有幾個高優先級的線程沒有進入非活動狀態,低優先級的線程可能永遠也不能執行。每當調度器決定運行一個新線程時,首先會在具備高優先級的線程中進行選擇,儘管這樣會使低優先級的線程徹底餓死。
void setPriority(int newPriority) //設置線程的優先級。優先級必須在Thread.MIN_PRIORITY與Thread.MAX_PRIORITY之間。通常使用Thread.NORMJ»RIORITY優先級 static int MIN_PRIORITY //線程的最小優先級。最小優先級的值爲1。 static int N0RM_PRI0RITY //線程的默認優先級。默認優先級爲5 static int MAX_PRIORITY //線程的最高優先級。最高優先級的值爲10 static void yield() //致使當前執行線程處於讓步狀態。若是有其餘的可運行線程具備至少與此線程一樣高 的優先級,那麼這些線程接下來會被調度。注意,這是一個靜態方法
有一種線程,他是在後臺運行的,他的任務就是爲其餘的線程提供服務,這種線程被稱爲守護線程,好比JVM的垃圾回收線程。
能夠經過調用
t.setDaemon(true);
將線程轉換爲守護線程(daemon thread)。這樣一個線程沒有什麼神奇。守護線程的惟一用途是爲其餘線程提供服務。計時線程就是一個例子,它定時地發送「計時器嘀嗒」信號給其餘 線程或清空過期的高速緩存項的線程。當只剩下守護線程時,虛擬機就退出了,因爲若是隻剩下守護線程,就不必繼續運行程序了。
守護線程有時會被初學者錯誤地使用, 他們不打算考慮關機(shutdown) 動做。可是,這是很危險的。守護線程應該永遠不去訪問固有資源,如文件、數據庫,由於它會在任什麼時候候甚至在一個操做的中間發生中斷。
線程的run方法不能拋出任何受查異常,可是,非受査異常會致使線程終止。在這種狀況下,線程就死亡了。
可是,不須要任何catch子句來處理能夠被傳播的異常。相反,就在線程死亡以前,異常被傳遞到一個用於未捕獲異常的處理器。
該處理器必須屬於一個實現Thread.UncaughtExceptionHandler
接口的類。這個接口只有一個方法。
void uncaughtException(Thread t, Throwable e)
能夠用setUncaughtExceptionHandler
方法爲任何線程安裝一個處理器。也能夠用Thread類的靜態方法setDefaultUncaughtExceptionHandler
爲全部線程安裝一個默認的處理器。替換處理器可使用日誌API發送未捕獲異常的報告到日誌文件。
若是不安裝默認的處理器,默認的處理器爲空。可是,若是不爲獨立的線程安裝處理器,此時的處理器就是該線程的ThreadGroup對象。
註釋:線程組是一個能夠統一管理的線程集合。默認狀況下,建立的全部線程屬於相同的線程組,可是,也可能會創建其餘的組。如今引入了更好的特性用於線程集合的操做,因此建議不要在本身的程序中使用線程組。
ThreadGroup類實現Thread.UncaughtExceptionHandler
接口。它的uncaughtException方法作以下操做:
Thread.getDefaultExceptionHandler
方法返回一個非空的處理器,則調用該處理器。4)不然,線程的名字以及Throwable的棧軌跡被輸出到System.err
上。這是你在程序中確定看到過許屢次的棧軌跡。
若是須要讓當前正在執行的線程暫停一段時間,進入阻塞狀態,則能夠經過調用Thread類的靜態sleep()方法來實現。sleep()方法有兩種重載方式。
static void sleep(long millis); //讓當前正在執行的線程暫停millis毫秒,並進入阻塞狀態 static void sleep(long millis,int nanos); ////讓當前正在執行的線程暫停millis毫秒+nanos毫秒,並進入阻塞狀態
噹噹前線程調用sleep()方法進入阻塞狀態後,在睡眠時間段內,該線程不會得到執行的機會,即便系統中沒有其餘可執行的線程,處於sleep()中的線程也不會執行,所以sleep()方法經常使用來暫停程序的執行。
在大多數實際的多線程應用中,兩個或兩個以上的線程須要共享對同一數據的存取。若是兩個線程存取相同的對象,而且每個線程都調用了一個修改該對象狀態的方法,將會發生什麼呢?能夠想象,線程彼此踩了對方的腳。根據各線程訪問數據的次序,可能會產生訛誤的對象。這樣一個狀況一般稱爲競爭條件(race condition)。
爲了不多線程引發的對共享數據的說誤,必須學習如何同步存取。在本節中,你會看到若是沒有使用同步會發生什麼。在下一節中,將會看到如何同步數據存取。在下面的測試程序中,模擬一個有若干帳戶的銀行。隨機地生成在這些帳戶之間轉移錢款的交易。每個帳戶有一個線程。每一筆交易中,會從線程所服務的帳戶中隨機轉移必定數目的錢款到另外一個隨機帳戶。模擬代碼很是直觀。咱們有具備transfer方法的Bank類。該方法從一個帳戶轉移必定數目的錢款到另外一個帳戶(尚未考慮負的帳戶餘額)。以下是Bank類的transfer方法的代碼。
public void transfer(int from, int to, double amount) // CAUTION: unsafe when called from multiple threads { System.out.print(Thread,currentThread()); accounts[from] -= amount; System.out.printf("%10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf("Total Balance: %10.2fXn", getTotalBalance()); }
這裏是Runnable類的代碼。它的run方法不斷地從一個固定的銀行帳戶取出錢款。在每一次迭代中,run方法隨機選擇一個目標帳戶和一個隨機帳戶,調用bank對象的transfer方法,而後睡眠。
Runnable r = () -> { try { while (true) { int toAccount = (int) (bank.size() * Math.random()); double amount = MAX_AMOUNT * Math.random(); bank.transfer(fromAccount, toAccount, amount); Thread.sleep((int) (DELAY * Math.random())); } } catch (InterruptedExeeption e) { } };
當這個模擬程序運行時,不清楚在某一時刻某一銀行帳戶中有多少錢。可是,知道全部帳戶的總金額應該保持不變,由於所作的一切不過是從一個帳戶轉移錢款到另外一個帳戶。在每一次交易的結尾,transfer方法從新計算總值並打印出來。本程序永遠不會結束。只能按CTRL+C來終止這個程序。
下面是典型的輸出:
Thread[Thread-11,5,main] 588.48 from 11to 44 Total Balance: 100000.00 Thread[Thread-12,5,main] 976.11from 12 to 22 Total Balance: 100000.00 Thread[Thread-14,5,main] 521.51 from 14 to 22 Total Balance: 100000.00 Thread[Thread-13,5,main] 359.89 from 13 to 81Total Balance: 100000.00 ... Thread[Thread-36,5,main] 401.71from 36 to 73 Total Balance: 99291.06 Thread[Thread-35,5,main] 691.46 from 35 to 77 Total Balance: 99291.06 Thread[Thread-37,5,main] 78.64 from 37 to 3 Total Balance: 99291.06 Thread[Thread-34,5,main] 197.11from 34 to 69 Total Balance: 99291.06 Thread[Thread-36,5,main] 85.96 from 36 to 4 Total Balance: 99291.06 Thread[Thread-4,5,main]Thread[Thread-33,5,main] 7.31 from 31to 32 Total Balance: 99979.24 627.50 from 4 to 5 Total Balance: 99979.24
正如前面所示,出現了錯誤。在最初的交易中,銀行的餘額保持在$100000, 這是正確的,由於共100個帳戶,每一個帳戶$1000。可是,過一段時間,餘額總量有輕微的變化。當運行這個程序的時候,會發現有時很快就出錯了,有時很長的時間後餘額發生混亂。這樣的狀態不會帶來信任感,人們極可能不肯意將辛苦掙來的錢存到這個銀行。程序清單14-5和程序清單14-6中的程序提供了完整的源代碼。看看是否能夠從代碼中找出問題。下一節將解說其中奧祕。
上一節中運行了一個程序,其中有幾個線程更新銀行帳戶餘額。一段時間以後,錯誤不知不覺地出現了,總額要麼增長,要麼變少。當兩個線程試圖同時更新同一個帳戶的時候,這個問題就出現了。假定兩個線程同時執行指令accounts[to] += amount
; 問題在於這不是原子操做。該指令可能被處理以下:
如今,假定第1個線程執行步驟1和2, 而後,它被剝奪了運行權。假定第2個線程被喚醒並修改了accounts數組中的同一項。而後,第1個線程被喚醒並完成其第3步。這樣,這一動做擦去了第二個線程所作的更新。因而,總金額再也不正確。咱們的測試程序檢測到這一訛誤。(固然,若是線程在運行這一測試時被中斷,也有可能會出現失敗警告!)
出現這一訛誤的可能性有多大呢?這裏經過將打印語句和更新餘額的語句交織在一塊兒執行,增長了發生這種狀況的機會。
若是刪除打印語句,訛誤的風險會下降一點,由於每一個線程在再次睡眠以前所作的工做不多,調度器在計算過程當中剝奪線程的運行權可能性很小。可是,訛誤的風險並無徹底消失。若是在負載很重的機器上運行許多線程,那麼,即便刪除了打印語句,程序依然會出錯。這種錯誤可能會幾分鐘、幾小時或幾天出現一次。坦白地說,對程序員而言,不多有比無規律出現錯誤更糟的事情了。
真正的問題是transfer方法的執行過程當中可能會被中斷。若是可以確保線程在失去控制以前方法運行完成,那麼銀行帳戶對象的狀態永遠不會出現訛誤。
有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供一個synchronized關鍵字達 到這一目的,而且Java SE 5.0引入了ReentrantLock類。synchronized關鍵字自動提供一個鎖以及相關的「條件」,對於大多數須要顯式鎖的狀況,這是很便利的。可是,咱們相信在讀者分別閱讀了鎖和條件的內容以後,理解 synchronized關鍵字是很輕鬆的事情。
java.util.concurrent
框架爲這些基礎機制提供獨立的類,在此以及第14.5.4節加以解釋這個內容。讀者理解了這些構建塊以後,將討論第14.5.5節。
用ReentrantLock保護代碼塊的基本結構以下:
myLock.lock(); // a ReentrantLock object try { critical section } finally { myLock.unlock();// make sure the lock is unlocked even if an exception is thrown }
這一結構確保任什麼時候刻只有一個線程進入臨界區。一旦一個線程封鎖了鎖對象,其餘任何線程都沒法經過lock語句。當其餘線程調用lock時,它們被阻塞,直到第一個線程釋放鎖對象。
警告:把解鎖操做括在finally子句以內是相當重要的。若是在臨界區的代碼拋出異常,鎖必須被釋放。不然,其餘線程將永遠阻塞。
註釋:若是使用鎖,就不能使用帶資源的try語句。首先,解鎖方法名不是close。不過,即便將它重命名,帶資源的try語句也沒法正常工做。它的首部但願聲明一個新變量。可是若是使用一個鎖,你可能想使用多個線程共享的那個變量(而不是新變量)。
讓咱們使用一個鎖來保護Bank類的transfer方法。
public class Bank { private Lock bankLock = new ReentrantLock();// ReentrantLock implements the Lock interface public void transfer(int from, int to, int amount) { bankLock.lock(); try { System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf("%10.2f from %A to %d", amount, from, to); accounts[to] += amount; System.out.printf("Total Balance: %10.2f%n", getTotalBalance()); } finally { banklock.unlock(); } } }
假定一個線程調用transfer,在執行結束前被剝奪了運行權。假定第二個線程也調用transfer,因爲第二個線程不能得到鎖,將在調用lock方法時被阻塞。它必須等待第一個線程完成transfer方法的執行以後才能再度被激活。當第一個線程釋放鎖時,那麼第二個線程才能開始運行(見圖 14-5)。
嘗試一下。添加加鎖代碼到transfer方法而且再次運行程序。你能夠永遠運行它,而銀行的餘額不會出現訛誤。
注意每個Bank對象有本身的ReentrantLock對象。若是兩個線程試圖訪問同一個Bank對象,那麼鎖以串行方式提供服務。可是,若是兩個線程訪問不一樣的Bank 對象,每個線程獲得不一樣的鎖對象,兩個線程都不會發生阻塞。本該如此,由於線程在操縱不一樣的Bank實例的時候,線程之間不會相互影響。
鎖是可重入的,由於線程能夠重複地得到已經持有的鎖。鎖保持一個持有計數(hold count)來跟蹤對lock方法的嵌套調用。線程在每一次調用lock都要調用unlock來釋放鎖。因爲這一特性,被一個鎖保護的代碼能夠調用另外一個使用相同的鎖的方法。
例如,transfer方法調用getTotalBalance方法,這也會封鎖bankLock對象,此時bankLock對象的持有計數爲2。當getTotalBalance方法退出的時候,持有計數變回1。當transfer方法退出的時候,持有計數變爲0。線程釋放鎖。一般,可能想要保護需若干個操做來更新或檢查共享對象的代碼塊。要確保這些操做完成後,另外一個線程才能使用相同對象。
警告:要留心臨界區中的代碼,不要由於異常的拋出而跳出臨界區。若是在臨界區代碼 結束以前拋出了異常,finally子句將釋放鎖,但會使對象可能處於一種受損狀態。
java.util.concurrent.locks.Lock 5.0
void lock(); //獲取這個鎖;若是鎖同時被另外一個線程擁有則發生阻塞。 void unlock(); //釋放這個鎖。
java.util.concurrent.locks.ReentrantLock 5.0
ReentrantLock(); //構建一個能夠被用來保護臨界區的可重入鎖。 ReentrantLock(boolean fair); //構建一個帶有公平策略的鎖。一個公平鎖偏心等待時間最長的線程。可是,這一公平的保證將大大下降性能。因此,默認狀況下,鎖沒有被強制爲公平的。
警告: 聽起來公平鎖更合理一些,可是使用公平鎖比使用常規鎖要慢不少。 只有當你確實瞭解本身要作什麼而且對於你要解決的問題有一個特定的理由必須使用公平鎖的時候,纔可使用公平鎖。即便使用公平鎖,也沒法確保線程調度器是公平的。若是線程調度 器選擇忽略一個線程,而該線程爲了這個鎖已經等待了很長時間,那麼就沒有機會公平地處理這個鎖了。
一般,線程進入臨界區,卻發如今某一條件知足以後它才能執行。要使用一個條件對象來管理那些已經得到了一個鎖可是卻不能作有用工做的線程。在這一節裏,咱們介紹Java庫中條件對象的實現。(因爲歷史的緣由, 條件對象常常被稱爲條件變量(conditional variable)。 )如今來細化銀行的模擬程序。咱們避免選擇沒有足夠資金的帳戶做爲轉出帳戶。注意不能使用下面這樣的代碼:
if (bank.getBalance(from) >= amount) bank.transfer(from, to, amount);
當前線程徹底有可能在成功地完成測試,且在調用transfer方法以前將被中斷。
if (bank.getBalance(from) >= amount) // thread night be deactivated at this point bank.transfer(from, to, amount);
在線程再次運行前,帳戶餘額可能已經低於提款金額。必須確保沒有其餘線程在本檢査餘額 與轉帳活動之間修改餘額。經過使用鎖來保護檢査與轉帳動做來作到這一點:
public void transfer(int from, int to,int amount) { bankLock.lock(); try { while (accounts[from] < amount) { // wait ... } // transfer funds ... } finally { bankLock.unlock(); } }
如今,當帳戶中沒有足夠的餘額時,應該作什麼呢?等待直到另外一個線程向帳戶中注入了資金。可是,這一線程剛剛得到了對bankLock的排它性訪問,所以別的線程沒有進行存款操做的機會。這就是爲何咱們須要條件對象的緣由。
一個鎖對象能夠有一個或多個相關的條件對象。你能夠用newCondition方法得到一個條件對象。習慣上給每個條件對象命名爲能夠反映它所表達的條件的名字。例如,在此設置一個條件對象來表達「餘額充足」條件。
class Bank { private Condition sufficientFunds; ... public Bank() { ... sufficientFunds = bankLock.newCondition(); } }
若是transfer方法發現餘額不足,它調用
sufficientFunds.await();
當前線程如今被阻塞了,並放棄了鎖。咱們但願這樣可使得另外一個線程能夠進行增長帳戶餘額的操做。
等待得到鎖的線程和調用await方法的線程存在本質上的不一樣。一旦一個線程調用await方法,它進入該條件的等待集。當鎖可用時,該線程不能立刻解除阻塞。相反,它處於阻塞狀態,直到另外一個線程調用同一條件上的signalAll方法時爲止。
當另外一個線程轉帳時,它應該調用
sufficientFunds.signalAll();
這一調用從新激活由於這一條件而等待的全部線程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖從新進入該對象。一旦鎖成爲可用的,它們中的某個將從await調用返回,得到該鎖並從被阻塞的地方繼續執行。此時,線程應該再次測試該條件。因爲沒法確保該條件被知足---signalAll方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢測該條件。
註釋: 一般,對await的調用應該在以下形式的循環體中
while (!(ok to proceed)) condition.await();
相當重要的是最終須要某個其餘線程調用signalAll方法。當一個線程調用await時,它沒有辦法從新激活自身。它寄但願於其餘線程。若是沒有其餘線程來從新激活等待的線程,它就永遠再也不運行了。這將致使使人不快的死鎖(deadlock) 現象。若是全部其餘線程被阻塞,最後一個活動線程在解除其餘線程的阻塞狀態以前就調用await方法,那麼它也被阻塞。沒有任何線程能夠解除其餘線程的阻塞,那麼該程序就掛起了。
應該什麼時候調用signalAll呢?經驗上講,在對象的狀態有利於等待線程的方向改變時調用signalAll。例如,當一個帳戶餘額發生改變時,等待的線程會應該有機會檢查餘額。在例子中,當完成了轉帳時,調用signalAll方法。
public void transfer(int from, int to, int amount) { bankLock.lock(); try { while (accounts[from] < amount) sufficientFunds.await(); // transfer funds sufficientFunds.signalAll(); } finally { bankLock.unlock(); } }
注意調用signalAll不會當即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法以後,經過競爭實現對對象的訪問。
另外一個方法signal, 則是隨機解除等待集中某個線程的阻塞狀態。這比解除全部線程的 阻塞更加有效,但也存在危險。若是隨機選擇的線程發現本身仍然不能運行,那麼它再次被阻塞。若是沒有其餘線程再次調用signal, 那麼系統就死鎖了。
警告:當一個線程擁有某個條件的鎖時,它僅僅能夠在該條件上調用await、signalAll或signal方法。
若是你運行程序清單14-7中的程序,會注意到沒有出現任何錯誤。總餘額永遠是$100 000。
沒有任何帳戶曾出現負的餘額(可是,你仍是須要按下CTRL+C鍵來終止程序)。你可能還注意到這個程序運行起來稍微有些慢---這是爲同步機制中的簿記操做所付出的代價。
實際上,正確地使用條件是富有挑戰性的。在開始實現本身的條件對象以前,應該考慮使用14.10節中描述的結構。
package synch; import java.util.*; import java.util.concurrent.locks.*; /** * A bank with a number of bank accounts that uses locks for serializing access. * ©version 1.30 2004-08-01 * ©author Cay Horstmann */ public class Bank { private final double[] accounts; private Lock bankLock; private Condition sufficientFunds; /** * Constructs the bank. * @param n the number of accounts * @param initialBalance the initial balance for each account */ public Bank(int n, double initialBalance) { accounts = new double[n]; Arrays.fill(accounts, initialBalance); bankLock = new ReentrantLock(); sufficientFunds = bankLock.newCondition(); /** * Transfers money from one account to another. * @param from the account to transfer from * @param to the account to transfer to * @paran amount the amount to transfer */ public void transfer(int from, int to, double amount) throws InterruptedException { bankLock.lock(); try { while (accounts[from] < amount) sufficientFunds.await(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf("%10.2f from %6 to %d", amount, from, to); accounts[to] += amount; System.out.printf("Total Balance: %10.2f%n", getTotalBalance()); sufficientFunds.signalAll(); } finally { bankLock.unlock(); } } /** * Gets the sum of all account balances. * ©return the total balance */ public double getTotalBalance() { bankLock.lock(); try { double sum = 0; for (double a:accounts) sum += a; return sum; } finally { bankLock.unlock(); } } /** * Gets the number of accounts in the bank. * ©return the number of accounts */ public int size() { return accounts.length; } }
在前面一節中,介紹瞭如何使用Lock和Condition對象。在進一步深刻以前,總結一下有關鎖和條件的關鍵之處:
Lock和Condition接口爲程序設計人員提供了高度的鎖定控制。然而,大多數狀況下,並不須要那樣的控制,而且可使用一種嵌入到Java語言內部的機制。從1.0版開始,Java中的每個對象都有一個內部鎖。若是一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須得到內部的對象鎖。
換句話說,
public synchronized void method() { method body }
等價於
public void method() { this.intrinsidock.lock(); try { method body } finally { this.intrinsicLock.unlock(); } }
例如,能夠簡單地聲明Bank類的transfer方法爲synchronized, 而不是使用一個顯式的鎖。內部對象鎖只有一個相關條件。wait方法添加一個線程到等待集中,notifyAll/notify
方法解除等待線程的阻塞狀態。換句話說,調用wait
或notityAll
等價於
intrinsicCondition.await(); intrinsicCondition.signalAll();
註釋:
wait
、notifyAll
以及notify
方法是Object類的final方法。Condition方法必須被命名爲await
、signalAll
和signal
以便它們不會與那些方法發生衝突。
例如,能夠用Java實現Bank類以下:
class Bank { private double[] accounts; public synchronized void transfer(int from,int to, int amount) throws InterruptedException { while (accounts[from] < amount) wait(); // wait on intrinsic object lock's single condition accounts[from] -= amount; accounts[to] += amount; notifyAll();// notify all threads waiting on the condition } public synchronized double getTotalBalance() { ... } }
能夠看到,使用synchronized關鍵字來編寫代碼要簡潔得多。固然,要理解這一代碼,你必須瞭解每個對象有一個內部鎖, 而且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程。
提示:Synchronized方法是相對簡單的。可是,初學者經常對條件感到困惑。在使用wait/notifyAll以前,應該考慮使用第14.10節描述的結構之一。
將靜態方法聲明爲synchronized也是合法的。若是調用這種方法,該方法得到相關的類對象的內部鎖。例如,若是Bank類有一個靜態同步的方法,那麼當該方法被調用時,Bankxlass對象的鎖被鎖住。所以,沒有其餘線程能夠調用同一個類的這個或任何其餘的同步靜態方法。
內部鎖和條件存在一些侷限。包括:
在代碼中應該使用哪種?Lock和Condition對象仍是同步方法?下面是一些建議:
程序清單14-8 synch2/Bank.java
package synch2; import java.util.*; /** * A bank with a number of bank accounts that uses synchronization primitives. * ©version 1.30 2004-08-01 s * ©author Cay Horstmann */ public class Bank { private final doublet[] accounts; /** * Constructs the bank. * @parain n the number of accounts * @param initialBalance the initial balance for each account */ public Bank(int n, double initialBalance) { accounts = new double[n]; Arrays.fill (accounts, initialBalance); } /** Transfers money from one account to another. * @param from the account to transfer from * @param to the account to transfer to * @param amount the amount to transfer */ public synchronized void transfer(int from, int to, double amount) throws InterruptedException { while (accounts[from] < amount) wait(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalanceO); notifyAll(); } /** * Gets the sum of all account balances. * return the total balance */ public synchronized double getTotalBalance() { double sum = 0; for (double a : accounts) sum += a; return sum; } /** * Gets the number of accounts in the bank. * ©return the number of accounts */ public int size() { return accounts.length; } }
java.lang.Object 1.0
void notifyAll(); //解除那些在該對象上調用wait方法的線程的阻塞狀態。該方法只能在同步方法或同步塊內部調用。若是當前線程不是對象鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。 void notify(); //隨機選擇一個在該對象上調用wait方法的線程,解除其阻塞狀態。該方法只能在一個同步方法或同步塊中調用。若是當前線程不是對象鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。 void wait(); //致使線程進入等待狀態直到它被通知。該方法只能在一個同步方法中調用。若是當前線程不是對象鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。 void wait(long millis); void wait(long millis, int nanos); //致使線程進入等待狀態直到它被通知或者通過指定的時間。這些方法只能在一個同步方法中調用。若是當前線程不是對象鎖的持有者該方法拋出一個IllegalMonitorStateException異常。 參數 millis 毫秒數 nanos 納秒數,<1 000 000
正如剛剛討論的,每個Java對象有一個鎖。線程能夠經過調用同步方法得到鎖。還有另外一種機制能夠得到鎖,經過進入一個同步阻塞。當線程進入以下形式的阻塞:
synchronized (obj) // this is the syntax for a synchronized block { critical section }
因而它得到Obj的鎖。有時會發現「特殊的」鎖,例如:
public class Bank { private doublet[] accounts; private Object lock = new Object(); public void transfer(int from, int to, int amount) { synchronized (lock) // an ad-hoc lock { accounts[from] -= amount; accounts[to] += amount; } System.out.println(...) } }
在此,lock
對象被建立僅僅是用來使用每一個Java對象持有的鎖。
有時程序員使用一個對象的鎖來實現額外的原子操做,實際上稱爲客戶端鎖定(clientside locking) 例如,考慮Vector類,一個列表,它的方法是同步的。如今,假定在Vector<Double>
中存儲銀行餘額。這裏有一個transfer
方法的原始實現:
public void transfer(Vector<Double> accounts, int from, int to, int amount)// Error { accounts.set(from, accounts.get(from)-amount); accounts.set(to, accounts.get(to)+ amount); System.out.println(...); }
Vector類的get和set方法是同步的,可是,這對於咱們並無什麼幫助。在第一次對get的調用已經完成以後,一個線程徹底可能在transfer方法中被剝奪運行權。因而,另外一個線程可能在相同的存儲位置存入不一樣的值。可是,咱們能夠截獲這個鎖:
public void transfer(Vector<Double> accounts, int from, int to, int amount) { synchronized(accounts) { accounts.set(from, accounts.get(from)- amount); accounts.set(to, accounts.get(to)+ amount); } System.out.println(...); }
這個方法能夠工做,可是它徹底依賴於這樣一個事實,Vector類對本身的全部可修改方法都使用內部鎖。然而,這是真的嗎?Vector類的文檔沒有給出這樣的承諾。不得不仔細研究源代碼並但願未來的版本能介紹非同步的可修改方法。如你所見,客戶端鎖定是很是脆弱的,一般不推薦使用。
鎖和條件是線程同步的強大工具,可是,嚴格地講,它們不是面向對象的。多年來,研究人員努力尋找一種方法,能夠在不須要程序員考慮如何加鎖的狀況下,就能夠保證多線程的安全性。最成功的解決方案之一是監視器(monitor), 這一律念最先是由PerBrinchHansen和TonyHoare在20世紀70年代提出的。用Java的術語來說,監視器具備以下特性:
obj.method()
, 那麼obj對象的鎖是在方法調用開始時自動得到,而且當方法返回時自動釋放該鎖。由於全部的域是私有的,這樣的安排能夠確保一個線程在對對象操做時, 沒有其餘線程能訪問該域。監視器的早期版本只有單一的條件,使用一種很優雅的句法。能夠簡單地調用await accounts[from] >= balance
而不使用任何顯式的條件變量。然而,研究代表盲目地從新測試條件是低效的。顯式的條件變量解決了這一問題。每個條件變量管理一個獨立的線程集。
Java設計者以不是很精確的方式採用了監視器概念,Java中的每個對象有一個內部的鎖和內部的條件。若是一個方法用synchronized關鍵字聲明,那麼,它表現的就像是一個監視器方法。經過調用wait/notifyAll/notify
來訪問條件變量。
然而,在下述的3個方面Java對象不一樣於監視器,從而使得線程的安全性降低:
這種對安全性的輕視激怒了Per Brinch Hansen。他在一次對原始Java中的多線程的嚴厲評論中,寫道:「這實在是令我震驚,在監視器和併發Pascal出現四分之一個世紀後,Java的這種不安全的並行機制被編程社區接受。這沒有任何益處。」 [Java’ s Insecure Parallelism, ACM SIGPLANNotices 34:38-45, April 1999.]
有時,僅僅爲了讀寫一個或兩個實例域就使用同步,顯得開銷過大了。畢竟,什麼地方能出錯呢?遺憾的是,使用現代的處理器與編譯器,出錯的可能性很大。
若是你使用鎖來保護能夠被多個線程訪問的代碼, 那麼能夠不考慮這種問題。編譯 器被要求經過在必要的時候刷新本地緩存來保持鎖的效應,而且不能不正當地從新排序 指令。詳細的解釋見JSR 133的Java內存模型和線程規範(參看http://www.jcp.org/en/jsr/detail?id=133) 該規範的大部分很複雜並且技術性強,可是文檔中也包含了不少解釋得很清晰的例子。在http://www-106.ibm.com/developerworks/java/library/j-jtp02244.html 有Brian Goetz寫的一個更易懂的概要介紹。
註釋:Brian Goetz給出了下述 「同步格言」:「若是向一個變量寫入值,而這個變量接下來可能會被另外一個線程讀取,或者,從一個變量讀值,而這個變量多是以前被另外一個線程寫入的,此時必須使用同步」。
volatile關鍵字爲實例域的同步訪問提供了一種免鎖機制。若是聲明一個域爲volatile, 那麼編譯器和虛擬機就知道該域是可能被另外一個線程併發更新的。
例如,假定一個對象有一個布爾標記done, 它的值被一個線程設置卻被另外一個線程査詢,如同咱們討論過的那樣,你可使用鎖:
private boolean done; public synchronized boolean isDone() { return done; } public synchronized void setDone() { done = true; }
或許使用內部鎖不是個好主意。若是另外一個線程已經對該對象加鎖,isDone和setDone方法可能阻塞。若是注意到這個方面,一個線程能夠爲這一變量使用獨立的Lock。可是,這也會帶來許多麻煩。
在這種狀況下,將域聲明爲volatile是合理的:
private volatile boolean done; public boolean isDone() { return done; } public void setDone() { done = true; }
警告:Volatile變量不能提供原子性。例如,方法
public void flipDone() { done = !done; } // not atomic不能確保翻轉域中的值。不能保證讀取、翻轉和寫入不被中斷。
上一節已經瞭解到,除非使用鎖或volatile修飾符,不然沒法從多個線程安全地讀取一個域。
還有一種狀況能夠安全地訪問一個共享域,即這個域聲明爲final時。考慮如下聲明:
final Map<String, Double> accounts = new HashKap<>();
其餘線程會在構造函數完成構造以後纔看到這個accounts變量。
若是不使用 final,就不能保證其餘線程看到的是accounts更新後的值,它們可能都只是看到null, 而不是新構造的 HashMap。
固然,對這個映射表的操做並非線程安全的。若是多個線程在讀寫這個映射表,仍然須要進行同步。
鎖和條件不能解決多線程中的全部問題。考慮下面的狀況:
帳戶 1: $200 帳戶 2: $300 線程 1: 從帳戶 1 轉移 $300 到帳戶 2 線程 2: 從帳戶 2 轉移 $400 到帳戶 1
如圖14-6所示,線程1和線程2都被阻塞了。由於帳戶1以及帳戶2中的餘額都不足以進行轉帳,兩個線程都沒法執行下去。
有可能會由於每個線程要等待更多的錢款存入而致使全部線程都被阻塞。這樣的狀態稱爲死鎖(deadlock)。
在這個程序裏,死鎖不會發生,緣由很簡單。每一次轉帳至多$1 000。由於有100個帳戶,並且全部帳戶的總金額是 $100 000, 在任意時刻,至少有一個帳戶的餘額髙於$1 000。從該帳戶取錢的線程能夠繼續運行。
可是,若是修改run方法,把每次轉帳至多$1 000的限制去掉,死鎖很快就會發生。試試看。將NACCOUNTS
設爲10。每次交易的金額上限設置爲2*INITIAL_BALANCE
, 而後運行該程序。程序將運行一段時間後就會掛起。
致使死鎖的另外一種途徑是讓第i個線程負責向第i個帳戶存錢,而不是從第i個帳戶取錢。 這樣一來,有可能將全部的線程都集中到一個帳戶上,每個線程都試圖從這個帳戶中取出大於該帳戶餘額的錢。試試看。在SynchBankTest程序中,轉用TransferRunnable類的run方法。在調用transfer時,交換fromAccount和toAccount。運行該程序並查看它爲何會當即死鎖。
還有一種很容易致使死鎖的狀況: 在SynchBankTest程序中, 將signalAll方法轉換爲signal, 會發現該程序最終會掛起(將 NACCOUNTS設爲10能夠更快地看到結果)。signalAll通知全部等待增長資金的線程,與此不一樣的是signal方法僅僅對一個線程解鎖。若是該線程不能繼續運行,全部的線程可能都被阻塞。考慮下面這個會發生死鎖的例子。
帳戶1 :$1990 全部其餘帳戶:每個 $990 線程 1: 從帳戶 1 轉移 $995 到帳戶 2 全部其餘線程: 從他們的帳戶轉移 $995 到另外一個帳戶
顯然,除了線程1, 全部的線程都被阻塞,由於他們的帳戶中沒有足夠的餘額。
線程1繼續執行,運行後出現以下情況:
帳戶 1: $995 帳戶 2: $1985 全部其餘帳戶:每一個 $990
而後,線程1調用signal。signal方法隨機選擇一個線程爲它解鎖。假定它選擇了線程3。該線程被喚醒,發如今它的帳戶裏沒有足夠的金額,它再次調用await。可是,線程1仍在運行,將隨機地產生一個新的交易,例如,
線程1 :從帳戶 1 轉移 $997 到帳戶 2
如今,線程1也調用await, 全部的線程都被阻塞。系統死鎖。問題的原由在於調用signal。它僅僅爲一個線程解鎖,並且,它極可能選擇一個不能繼續運行的線程(在咱們的例子中,線程2必須把錢從帳戶2中取出)遺憾的是,Java編程語言中沒有任何東西能夠避免或打破這種死鎖現象。必須仔細設計程序,以確保不會出現死鎖。
線程在調用lock方法來得到另外一個線程所持有的鎖的時候,極可能發生阻塞。應該更加謹慎地申請鎖。tryLock方法試圖申請一個鎖,在成功得到鎖後返回true, 不然,當即返回 false, 並且線程能夠當即離開去作其餘事情。
if (myLock.tryLock()) { // now the thread owns the lock try { ... } finally { myLock.unlock(); } } else // do something else
能夠調用tryLock時,使用超時參數,像這樣:
if (myLock.tryLock(100, TineUnit.MILLISECONDS)) ...
TimeUnit是一 枚舉類型,能夠取的值包括SECONDS
、MILLISECONDS
,MICROSECONDS
和NANOSECONDS
。
lock方法不能被中斷。若是一個線程在等待得到一個鎖時被中斷,中斷線程在得到鎖以前一直處於阻塞狀態。若是出現死鎖,那麼,lock方法就沒法終止。
然而,若是調用帶有用超時參數的tryLock, 那麼若是線程在等待期間被中斷,將拋出InterruptedException異常。這是一個很是有用的特性,由於容許程序打破死鎖。
也能夠調用locklnterruptibly方法。它就至關於一個超時設爲無限的tryLock方法。
在等待一個條件時,也能夠提供一個超時:
myCondition.await(100, TineUniBILLISECONDS))
若是一個線程被另外一個線程經過調用signalAU或signal激活,或者超時時限已達到,或者線程被中斷,那麼await方法將返回。
若是等待的線程被中斷,await方法將拋出一個InterruptedException異常。在你但願出現這種狀況時線程繼續等待(可能不太合理),可使用awaitUninterruptibly方法代替 await。
java.util.concurrent.locks.Lock 5.0
boolean tryLock(); //嘗試得到鎖而沒有發生阻塞;若是成功返回真。這個方法會搶奪可用的鎖,即便該鎖有公平加鎖策略,即使其餘線程已經等待好久也是如此。 boolean tryLock(long time, TimeUnit unit); //嘗試得到鎖,阻塞時間不會超過給定的值;若是成功返回 true。 void lockInterruptibly(); //得到鎖,可是會不肯定地發生阻塞。若是線程被中斷,拋出一個InterruptedException異常。
java.util.concurrent.locks.Condition 5.0
boolean await(long time, TimeUnit unit); //進入該條件的等待集,直到線程從等待集中移出或等待了指定的時間以後才解除阻塞。若是由於等待時間到了而返回就返回false, 不然返回true。 void awaitUninterruptibly(); //進入該條件的等待集,直到線程從等待集移出才解除阻塞。若是線程被中斷,該方法 不會拋出InterruptedException異常。
如今,讀者已經看到了造成Java併發程序設計基礎的底層構建塊。然而,對於實際編程來講,應該儘量遠離底層結構。使用由併發處理的專業人士實現的較高層次的結構要方便得多、要安全得多。
對於許多線程問題,能夠經過使用一個或多個隊列以優雅且安全的方式將其形式化。生產者線程向隊列插入元素, 消費者線程則取出它們。使用隊列,能夠安全地從一個線程向另 一個線程傳遞數據。例如,考慮銀行轉帳程序,轉帳線程將轉帳指令對象插入一個隊列中,而不是直接訪問銀行對象。另外一個線程從隊列中取出指令執行轉帳。只有該線程能夠訪問該銀行對象的內部。所以不須要同步。(固然,線程安全的隊列類的實現者不能不考慮鎖和條件,可是, 那是他們的問題而不是你的問題。
當試圖向隊列添加元素而隊列已滿,或是想從隊列移出元素而隊列爲空的時候,阻塞隊列(blocking queue)致使線程阻塞。在協調多個線程之間的合做時,阻塞隊列是一個有用的工具。工做者線程能夠週期性地將中間結果存儲在阻塞隊列中。其餘的工做者線程移出中間結果並進一步加以修改。隊列會自動地平衡負載。若是第一個線程集運行得比第二個慢, 第二個線程集在等待結果時會阻塞。若是第一個線程集運行得快,它將等待第二個隊列集遇上來。表14-1給出了阻塞隊列的方法
方法 | 正常動做 | t特殊狀況下的動做 |
---|---|---|
add | 添加一個元素 | 若是隊列滿,則拋出IllegalStateException異常 |
element | 返回隊列的頭元素 | 若是隊列空,拋出NoSuchElementException異常 |
offer | 添加一個元素並返回true | 若是隊列滿,返回false |
peek | 返回隊列的頭元素 | 若是隊列空,則返回null |
poll | 移出並返回隊列的頭元素 | 若是隊列空,則返回null |
put | 添加一個元素 | 若是隊列滿,則阻塞 |
remove | 移出並返回頭元素 | 若是隊列空。則拋出NoSuchElementException異常 |
take | 移出並返回頭元素 | 若是隊列空,則阻塞 |
阻塞隊列方法分爲如下3類,這取決於當隊列滿或空時它們的響應方式。若是將隊列看成線程管理工具來使用,將要用到put和take方法。當試圖向滿的隊列中添加或從空的隊列 中移出元素時,add、remove和element操做拋出異常。固然,在一個多線程程序中,隊列會在任什麼時候候空或滿,所以,必定要使用offer、poll和peek方法做爲替代。這些方法若是不能完成任務,只是給出一個錯誤提示而不會拋出異常。
註釋: poll和peek方法返回空來指示失敗。所以,向這些隊列中插入null值是非法的。
還有帶有超時的offer方法和poll方法的變體。例如,下面的調用:
boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);
嘗試在100毫秒的時間內在隊列的尾部插入一個元素。若是成功返回true;不然,達到超時時,返回false。相似地,下面的調用:
Object head = q.poll(100, TimeUnit.MILLISECONDS);
嘗試用100毫秒的時間移除隊列的頭元素;若是成功返回頭元素,不然,達到在超時時,返回null。
若是隊列滿,則put方法阻塞;若是隊列空,則take方法阻塞。在不帶超時參數時,offer和poll方法等效。
java.util.concurrent包提供了阻塞隊列的幾個變種。默認狀況下,LinkedBlockingQueue的容量是沒有上邊界的,可是,也能夠選擇指定最大容量。LinkedBlockingDeque是一個雙端的版本。ArrayBlockingQueue在構造時須要指定容量,而且有一個可選的參數來指定是否須要公平性。若設置了公平參數,則那麼等待了最長時間的線程會優先獲得處理。一般,公平性會下降性能,只有在確實很是須要時才使用它。
PriorityBlockingQueue是一個帶優先級的隊列,而不是先進先出隊列。元素按照它們的優先級順序被移出。該隊列是沒有容量上限,可是,若是隊列是空的,取元素的操做會阻塞。(有關優先級隊列的詳細內容參看第9章。 )
最後,DelayQueue包含實現Delayed接口的對象:
interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unit); }
getDelay方法返回對象的殘留延遲。負值表示延遲已經結束。元素只有在延遲用完的情 況下才能從DelayQueue移除。還必須實現compareTo方法。DelayQueue使用該方法對元素進行排序。
JavaSE 7增長了一個TranSferQueue接口,容許生產者線程等待,直到消費者準備就緒能夠接收一個元素。若是生產者調用
q.transfer(item);
這個調用會阻塞,直到另外一個線程將元素(item)刪除。LinkedTransferQueue類實現了這個接口。
程序清單14-9中的程序展現瞭如何使用阻塞隊列來控制一組線程。程序在一個目錄及它的全部子目錄下搜索全部文件,打印出包含指定關鍵字的行。
程序清單 14-9 blockingQueue/BlockingQueueTest.java
package blockingQueue; import java.io.*; import java.util.*; import java.util.concurrent.*; /** * ©version 1.02 2015-06-21 * author Cay Horstmann */ public class BlockingQueueTest { private static final int FILE_QUEUE_SIZE = 10; private static final int SEARCH_THREADS = 100; private static final File DUMMY = new File(""); private static BlockingQueue<File> queue = new ArrayBlockingQueueo(FILE_QUEUE_SIZE); public static void main(String[] args) { try (Scanner in = new Scanner(System.in)) { System.out.print("Enter base directory (e.g. /opt/jdkl.8.0/src): "); String directory = in.nextline(); System.out.print("Enter keyword (e.g. volatile): "); String keyword = in.nextLine(); Runnable enumerator = () -> { try { enumerate(new File(directory)); queue.put(DUMMY); } catch (InterruptedException e) { } }; new Thread(enumerator).start(); for (int i = 1 ; i <= SEARCH.THREADS; i++) { Runnable searcher = () -> { try { boolean done = false; while (!done) { File file = queue.take(); if (file = DUMMY) { queue.put(file); done = true; } else search(file, keyword); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { } }; new Thread(searcher).start(); } } } /** * Recursively enumerates all files in a given directory and its subdirectories. * @paran directory the directory in which to start */ public static void enumerate(File directory) throws InterruptedException { File[] files = directory.listFiles(); for (File file : files) { if (file.isDirectory()) enumerate(file); else queue.put(file); } } /** * Searches a file for a given keyword and prints all matching lines. * @param file the file to search * @param keyword the keyword to search for */ public static void search(File file, String keyword) throws IOException { try (Scanner in = new Scanner(file, "UTF-8")) { int lineNuinber = 0; while (in.hasNextLine()) { lineNumber++; String line = in.nextLine(); if (line,contains(keyword)) System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber,line); } } } }
生產者線程枚舉在全部子目錄下的全部文件並把它們放到一個阻塞隊列中。這個操做很快,若是沒有上限的話,很快就包含了全部找到的文件。
咱們同時啓動了大量搜索線程。每一個搜索線程從隊列中取出一個文件,打開它,打印全部包含該關鍵字的行,而後取出下一個文件。咱們使用一個小技巧在工做結束後終止這個應用程序。爲了發出完成信號,枚舉線程放置一個虛擬對象到隊列中(這就像在行李輸送帶上放一個寫着「最後一個包」的虛擬包)。當搜索線程取到這個虛擬對象時,將其放回並終止。
注意,不須要顯式的線程同步。在這個應用程序中,咱們使用隊列數據結構做爲一種同步機制。