1.競爭條件。在大多數實際的多線程應用中,兩個或兩個以上的線程須要共享對同一數據的存取。當兩個線程存取相同的對象,而且每個線程都調用了一個修改該對象狀態的方法的時候,根據各線程訪問數據的次序,可能會產生訛誤的對象。這樣的狀況一般稱爲「競爭條件」。爲了不多線程引發的對共享數據的訛誤,就必須學習如何「同步存取」。 java
2.鎖對象。從Java SE 5.0開始,有兩種機制防止代碼塊受併發訪問的干擾。即Java語言關鍵字synchronized和Java SE 5.0引入的ReentrantLock類。synchronized關鍵字自動提供一個鎖以及相關的「條件」(Condition),對於大多數須要顯式鎖的狀況,這是很便利的。使用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時,它們被阻塞,直到第一個線程釋放鎖對象。
3.條件對象。一般,線程進入臨界區,卻發如今某一條件知足以後它才能執行。這時候,就須要一個條件對象來管理那些已經得到了一個鎖可是卻不能作有用工做的線程(因爲歷史緣由,條件對象常常被稱爲條件變量(conditional variable))。
一個鎖對象能夠有一個或多個相關的條件對象,可使用newCondition方法得到一個條件對象。習慣上給每個條件對象命名爲能夠反映它所表達的條件的名字。
等待得到鎖的線程和調用await方法的線程存在本質上的不一樣。一旦一個線程調用await方法,它進入該條件的等待集。當鎖可用時,該線程不能立刻解除阻塞。相反,它處於阻塞狀態,直到另外一個線程調用同一條件上的signalAll方法時爲止。
當另外一條線程調用signalAll方法時,這一調用從新激活了由於這一條件而等待的全部線程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖從新進入該對象。一旦鎖成爲可用的,他們中的某個將從await調用返回,得到該鎖並從阻塞的地方繼續執行。此時,線程應該再次測試該條件。因爲沒法確保該條件被知足——signalAll方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢測該條件。
一般,await的調用應該在以下形式的循環體中:
編程
while(!(Ok to proceed)) condition.await();相當重要的是最終須要某個其它線程調用signalAll方法。當一個線程調用await時,它沒有辦法從新激活自身。它寄但願於其它線程,而若是沒有其它線程來從新激活等待的線程的話,它就永遠不在運行了。這將致使使人不快的死鎖(deathlock)現象。若是全部其它線程被阻塞,最後一個活動線程在解除其它線程的阻塞狀態以前就調用了await方法,那麼它也就被阻塞了。沒有任何線程能夠解除其它線程的阻塞,那麼該程序就掛起了。
4.synchronized關鍵字。咱們先總結一下鎖和條件的關鍵之處:1)鎖用來保護代碼片斷,任什麼時候刻只能有一個線程執行被保護的代碼。2)鎖能夠管理試圖進入被保護代碼段的線程。3)鎖能夠擁有一個或多個相關的條件對象。4)每一個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
Lock和Condition接口被添加到Java SE 5.0中,這也向程序設計人員提供了高度的封鎖機制。然而,大多數狀況下,並不須要那樣的控制,而且可使用一種嵌入到Java語言內部的機制。從1.0版開始,Java中的每個對象都有一個內部鎖。若是一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須得到內部的對象鎖。
換句話說,
數組
public synchronized void method(){ method body }等價於
public void method(){ this.intrinsicLock.lock(); try{ method body }finally{ this.intrinsicLock.unlock(); } }內部對象鎖只有一個相關條件。wait方法添加一個線程到等待集中,notifyAll/notify方法解除等待線程的阻塞狀態。換句話說,調用wait或者notifyAll方法等價於:
intrinsicCondition.await(); intrinsicCondition.signalAll();能夠看出,使用synchronized關鍵字來編寫代碼要簡潔的多。固然,要理解這些代碼,就必須瞭解每個對象有一個內部鎖,而且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程。
5.同步阻塞。每一個Java對象都有一個鎖。線程能夠經過調用同步方法得到鎖,也能夠經過進入一個同步阻塞,得到該對象的鎖: 緩存
synchronized(obj){ critical section }有時程序員使用一個對象的鎖來實現額外的原子操做,實際上被稱爲客戶端鎖定(client-side locking)。可是客戶端鎖定是很是脆弱的,一般不推薦使用。由於這種方法徹底依賴於這樣一個事實,obj對本身的全部可修改方法都使用內部鎖。
6.監視器概念。鎖和條件是線程同步的強大工具,可是嚴格地講,它們不是面向對象的。多年來,研究人員努力尋找一種方法,能夠在不須要程序員考慮如何加鎖的狀況下,就能夠保證多線程的安全性。最成功的解決方案之一是監視器(monitor),這一律念最先是由Per Brinch Hansen和Tony Hoare 在20世紀70年代提出的。用Java的術語講,監視器具備以下特性:
1)監視器是隻包含私有域的類。
2)每一個監視器類的對象有一個相關的鎖。
3)使用該鎖對全部的方法進行加鎖。換句話說,若是客戶端調用obj.method,那麼obj對象的鎖是在方法調用開始時自動得到,而且當方法返回時自動釋放該鎖。由於全部的域是私有的,這樣的安排能夠確保一個線程在對對象操做上,沒有其它線程能訪問該域。
4)該鎖能夠有任意多個相關條件。
監視器的早期版本只有單一的條件,不使用任何顯式的條件變量。然而,研究代表盲目地從新測試條件是低效的。顯式的條件變量解決了這一問題,每個條件變量管理一個獨立的線程集。
Java設計者以不是很精確的方式採用了監視器概念,Java中的每個對象有一個內部的鎖和內部的條件。若是一個方法用synchronized關鍵字聲明,那麼,它表現的就像是一個監視器方法。經過調用wait/notifyAll/notify來訪問條件變量。
然而,在下面三個方面Java對象不一樣於監視器,從而使得線程的安全性降低:
1)域不要求是私有的。
2)方法不要求必須是synchronized。
3)內部鎖對客戶是可用的。 安全
7.Volatile域。有時,僅僅爲了讀寫一個或兩個實例域就使用同步,顯得開銷過大了。那麼,什麼地方能出錯呢?遺憾的是,使用現代的處理器與編譯器,出錯的可能性很大:
1)多處理器的計算機可以暫時在寄存器或本地內存緩衝區中保存內存中的值。結果是,運行在不一樣處理器上的線程可能在同一個內存位置取到不一樣的值。
2)編譯器能夠改變指令執行的順序以使吞吐量最大化。這種順序上的變化不會改變代碼語義,可是編譯器假定內存的值僅僅在代碼中有顯式的修改指令時纔會改變。然而,內存的值能夠被另外一個線程改變!
若是你使用鎖來保護能夠被多個線程訪問的代碼,那麼能夠不考慮這種問題。編譯器被要求經過在必要的時候刷新本地緩存來保持鎖的效應,而且不能不正當地從新排序指令。詳細解釋參見JSR 133的Java內存模型和線程規範(http://www.jcp.org/en/jsr/detail?id=133)。
Brian Goetz給出了下述「同步格言」:若是向一個變量寫入值,而這個變量接下來可能會被另外一個線程讀取,或者,從一個變量讀值,而這個變量多是以前被另外一個線程寫入的,此時必須使用同步。
volatile關鍵字爲實例域的同步訪問提供了一種免鎖機制。若是聲明一個域爲volatile,那麼編譯器和虛擬機就知道該域是可能被另外一個線程併發更新的。
例如,假定一個對象有一個布爾標記done,它的值被一個線程設置卻被另外一個線程查詢,如以前所述,你可使用鎖: 數據結構
public synchronized boolean isDone(){ return done; } public synchronized void setDone(){ done = true; } private boolean done;或許使用內部鎖不是個好主意。若是另外一個線程已經對該對象加鎖,isDone和setDone方法可能阻塞。若是注意到這個方面,一個線程能夠爲這一變量使用獨立的Lock。可是,這也會帶來許多麻煩。
public boolean getDone(){ return done; } public void setDone(){ done = true; }; private volatile boolean done;這地方須要注意的一點是,Volatile不能提供原子性。例如,方法
public void flipDone(){ done = !done; }//not atomic不能確保改變域中的值。
8.死鎖。鎖和條件不能解決多線程中的全部問題,好比死鎖。在一個程序中,全部線程都被阻塞,這樣的狀態被稱爲死鎖。遺憾的是,Java編程語言中沒有任何東西能夠避免或打破這種死鎖現象。必須仔細設計程序,以確保不會出現死鎖。 多線程
9.鎖測試與超時。線程在調用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,TimeUnit.MILLISECONDS))......TimeUnit是一個枚舉類型,能夠取的值包括SECONDS、MILLISECONDS、MICROSECONDS和NANOSECONDS。
myCondition.await(100,TimeUnit.MILLISECONDS);若是一個線程被另外一個線程經過調用signalAll或signal激活,或者超時時限已達到,或者線程被中斷,那麼await方法將返回。
10.讀/寫鎖。java.util.concurrent.locks包定義了兩個鎖類,除了前面的ReentrantLock類,還有ReentrantReadWriteLock類。若是不少線程從一個數據結構讀取數據而不多線程修改其中數據的話,後者是十分有用的。在這種狀況下,容許對讀者線程共享訪問是合適的。固然,寫者線程依然必須是互斥訪問的。
下面是使用讀/寫鎖的必要步驟:
1)構造一個ReentrantReadWriteLock對象: 框架
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();2)抽取讀鎖和寫鎖:
private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();3)對全部的訪問者加讀鎖。
11.爲何棄用stop和suspend方法?初始的Java版本定義了一個stop方法用來終止一個線程,以及一個suspend方法用來阻塞一個線程直至另外一個線程調用resume。stop和suspend方法有一個共同點:都試圖控制一個給定線程的行爲。
從Java1.2開始就棄用了這兩個方法。stop方法天生就不安全,而經驗證實suspend方法會常常致使死鎖。
首先來看stop方法,該方法終止全部未結束的方法,包括run方法。當線程被終止,當即釋放被它鎖住的全部對象的鎖。這會致使對象處於不一致的狀態。例如,假定一個銀行轉帳的線程TransferThread,在從一個帳戶向另外一個帳戶轉帳的過程當中被終止,錢款已經轉出,卻沒有轉入目標帳戶,如今銀行對象就被破壞了,這種破壞會被其餘還沒有中止的線程觀察到。
當線程要終止另外一個線程時,沒法知道何時調用stop方法是安全的,何時會致使對象被破壞。所以,該方法被棄用了。在但願中止線程的時候應該中斷線程,被中斷的線程會在安全的時候中止。
接下來,看看suspend方法有什麼問題。與stop方法不一樣,suspend不會破壞對象。可是若是用suspend掛起一個持有一個鎖的線程,那麼,該鎖在恢復以前是不可用的。若是調用suspend方法的線程試圖得到同一個鎖,那麼程序死鎖:被掛起的線程等待恢復,而將其掛起的線程等待得到鎖。
若是想安全地掛起線程,引入一個變量suspendRequested並在run方法的某個安全的地方測試它,安全的地方是指該線程沒有封鎖其它線程須要的對象的地方。當該線程發現suspendRequested變量已經設置,將會保持等待狀態直到它再次得到爲止。下面的代碼框架實現這一設計:
public void run(){ while(...){ ... if(suspendRequested){ suspendLock.lock(); try{ while(suspendRequested) suspendCondition.await(); }finally{ suspendLock.unlock(); } } } } public void requestSuspend(){ suspendRequested = true; } public void requestResume(){ suspendRequested = false; suspendLock.lock(); try{ suspendCondition.signalAll(); }finally{ suspendLock.unlock(); } } private volatile boolean suspendRequested = false; private Lock suspendLock = new ReentrantLock(); private Condition suspendCondition = suspendLock.newCondition();