線程同步

        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時,它們被阻塞,直到第一個線程釋放鎖對象。
        鎖是可重入的(若一個程序或子程序能夠「安全的被並行執行」,則稱其爲「可重入」的),由於線程能夠重複地得到已經持有的鎖。鎖保持一個持有計數(hold count)來跟蹤對lock方法的嵌套調用。線程在每一次調用lock都要調用unlock來釋放鎖。因爲這一特性,被一個鎖保護的代碼能夠調用另外一個使用相同的鎖的方法。
     一般,可能想要保護需若干個操做來更新或檢查共享對象的代碼塊。要確保這些操做都完成後,另外一個線程才能使用相同對象。
        要留心臨界區中的代碼,不要由於異常的拋出而跳出了臨界區。若是在臨界區代碼結束以前拋出了異常,finally子句將釋放鎖,但會使對象處於一種受損狀態。
        另外,ReentrantLock還提供一個帶有公平策略的構造方法——ReentrantLock(boolean fair)。一個公平鎖偏心等待時間最長的線程。可是,這一公平的保證將大大下降性能。因此,默認狀況下,鎖沒有被強制爲公平的。不過,遺憾的是,即便使用公平鎖,也沒法確保線程調度器是公平的。


        3.條件對象。一般,線程進入臨界區,卻發如今某一條件知足以後它才能執行。這時候,就須要一個條件對象來管理那些已經得到了一個鎖可是卻不能作有用工做的線程(因爲歷史緣由,條件對象常常被稱爲條件變量(conditional variable))。
        一個鎖對象能夠有一個或多個相關的條件對象,可使用newCondition方法得到一個條件對象。習慣上給每個條件對象命名爲能夠反映它所表達的條件的名字。
        等待得到鎖的線程和調用await方法的線程存在本質上的不一樣。一旦一個線程調用await方法,它進入該條件的等待集。當鎖可用時,該線程不能立刻解除阻塞。相反,它處於阻塞狀態,直到另外一個線程調用同一條件上的signalAll方法時爲止。
        當另外一條線程調用signalAll方法時,這一調用從新激活了由於這一條件而等待的全部線程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖從新進入該對象。一旦鎖成爲可用的,他們中的某個將從await調用返回,得到該鎖並從阻塞的地方繼續執行。此時,線程應該再次測試該條件。因爲沒法確保該條件被知足——signalAll方法僅僅是通知正在等待的線程:此時有可能已經知足條件,值得再次去檢測該條件。
        一般,await的調用應該在以下形式的循環體中:
編程

    while(!(Ok to proceed))
        condition.await();
        相當重要的是最終須要某個其它線程調用signalAll方法。當一個線程調用await時,它沒有辦法從新激活自身。它寄但願於其它線程,而若是沒有其它線程來從新激活等待的線程的話,它就永遠不在運行了。這將致使使人不快的死鎖(deathlock)現象。若是全部其它線程被阻塞,最後一個活動線程在解除其它線程的阻塞狀態以前就調用了await方法,那麼它也就被阻塞了。沒有任何線程能夠解除其它線程的阻塞,那麼該程序就掛起了。
        從經驗上講,應該在對象的狀態有利於等待線程的方向改變時調用signalAll方法。
        注意:調用signalAll方法不會當即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法後,經過競爭實現對對象的訪問。
        另外一個方法signal,則是隨機解除等待集中某個線程的阻塞狀態。這比解除全部線程的阻塞更加有效,但也存在危險。若是隨機選擇的線程發現本身仍然不能運行,那麼它再次被阻塞。若是沒有其它線程再次調用signal,那麼系統就死鎖了。


        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的線程。
        將靜態方法聲明爲synchronized也是合法的。若是調用這種方法,該方法得到相關的類對象(Class)的內部鎖。所以,沒有其它線程能夠調用同一個類的這個或任何其它的同步靜態方法。
        內部鎖和條件存在一些侷限,包括:1)不能中斷一個正在試圖得到鎖的線程。2)試圖得到鎖時不能設置超時。3)每一個鎖僅有單一的條件,多是不夠的。
        在代碼中選擇使用Lock和Condition對象仍是同步方法的一些建議:1)最好既不使用Lock/Condition也不使用synchronized關鍵字。在許多狀況下你可使用java.util.concurrent包中的一種機制(阻塞隊列),它會爲你處理全部的加鎖。2)若是synchronized關鍵字適合你的程序,那麼請儘可能使用它,這樣能夠減小編寫的代碼數量,減小出錯的概率。3)若是特別須要Lock/Condition結構提供的獨有特性時,才使用Lock/Condition。


        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。可是,這也會帶來許多麻煩。
        在這種狀況下,就能夠將域聲明爲volatile:
public boolean getDone(){ return done; }
public void setDone(){ done = true; };
private volatile boolean done;
        這地方須要注意的一點是,Volatile不能提供原子性。例如,方法
public void flipDone(){ done = !done; }//not atomic
    不能確保改變域中的值。
        在這樣一種很是簡單的狀況下,存在第三種可能性,使用AtomicBoolean。這個類有方法get和set,且確保是原子的(就像它們是同步的同樣)。該實現使用有效的機器指令,在不使用鎖的狀況下確保原子性。在java.util.concurrent.atomic中有許多包裝器類用於原子的整數、浮點數、數組等。這些類是爲編寫併發實用程序的系統程序員提供使用的,而不是應用程序員。
        總之,在如下三個條件下,域的併發訪問是安全的:
        1)域是final的,而且在構造器調用完成以後被訪問。
        2)對域的訪問由公有的鎖進行保護。
        3)域是volatile的。
        語言的設計者試圖在優化使用volatile域的代碼的性能方面給實現人員留有餘地。可是,舊規範太複雜,實現人員難以理解,這帶來了混亂和非預期的行爲。例如,不可變對象不是真的不可變。


        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。
        lock方法不能被中斷。若是一個線程在等待得到一個鎖時被中斷,中斷線程在得到鎖以前一直處於阻塞狀態。若是出現死鎖,lock方法就沒法終止。
        然而,若是調用帶有超時參數的tryLock,那麼若是線程在等待期間被中斷,將拋出InterruptedException異常。這是一個很是有用的特性,由於容許程序打破死鎖。
        也能夠調用lockInterruptibly方法。它就至關於一個超時設爲無限的tryLock方法。
     在等待一個條件時,也能夠提供一個超時:
myCondition.await(100,TimeUnit.MILLISECONDS);
        若是一個線程被另外一個線程經過調用signalAll或signal激活,或者超時時限已達到,或者線程被中斷,那麼await方法將返回。
        若是等待的線程被中斷,await方法將拋出一個InterruptedException異常。在你但願出現這種狀況時線程繼續等待,可使用awaitUninterruptibly方法代替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)對全部的訪問者加讀鎖。
        4)對全部的修改者加寫鎖。

        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();
相關文章
相關標籤/搜索