《Java併發編程實戰》學習筆記

 

第2章 線程安全性

 

正確性:java

  某個類的行爲與其規範徹底一致。算法

 

2.1線程安全:數據庫

  當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類就能表現出正確的行爲,那麼就稱這個類是線程安全的。編程

 

無狀態對象數組

  既不包含任何域,也不包含任何其餘類中域的引用的對象。緩存

  無狀態對象必定是線程安全的。安全

 

競態條件:服務器

  當某個計算的正確性取決於多個線程的交替執行時序時,就會發生競態條件。網絡

  本質是基於一種可能失效的觀察結果來作出判斷或者執行某個計算。數據結構

  最多見的競態條件類型就是「先檢查後執行(Check-then-Act)」。

  「讀取-修改-寫入」操做也是一種競態條件。

 

2.2原子性:

  假定有兩個操做A和B,若是從執行A的線程來看,當另外一個線程執行B時,要麼將B所有執行完,要麼徹底不執行B,那麼A和B對彼此來講是原子的。

 

原子操做:

  對於訪問同一個狀態的全部操做(包括操做自己)來講,這個操做是一個以原子方式執行的操做。

  要保持狀態的一致性,就須要在單個原子操做中更新全部相關的狀態變量。

 

2.3內置鎖:

  每一個Java對象均可以用做一個實現同步的鎖,這些鎖被稱爲內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock)。

  內置鎖是一種互斥鎖,最多隻有一個線程持有這個鎖。

 

同步代碼塊(Synchronized Block)包括兩部分:

  一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。

  關鍵字Synchronized修飾方法就是一種同步代碼塊,鎖就是方法調用所在的對象,靜態的Synchronized方法以Class對象做爲鎖。

 

重入

  由於內置鎖是可重入的,因此若是某個線程試圖得到一個已經由它本身持有的鎖,那麼這個請求就會成功。

  重入意味着獲取鎖的操做粒度是「線程」,而不是「調用」。

  實現方式:爲每一個鎖關聯一個獲取計數值和一個全部者線程。當計數值爲0時,這個鎖被認爲沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,而且將獲取計數值置爲1。若是同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數值減1。

  重入提高了加鎖行爲的封裝性,所以簡化了面向對象併發代碼的執行。

 

2.4用鎖來保護狀態:

  鎖可以使其被保護的對象以串行方式來執行。

  對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都須要持有同一個鎖,在這種狀況下,咱們稱狀態變量是由這個鎖保護的。 

  對象的內置鎖與其狀態之間沒有內在的聯繫,雖然大多數類都將內置鎖用作一種有效的加鎖機制,但對象的域並不必定要經過內置鎖來保護

  一種常見的加鎖約定:將全部可變狀態都封裝在對象內部,並經過對象的內置鎖對全部訪問可變狀態的代碼路徑進行同步,使得該對象上不會發生併發訪問。

  對於每一個包含多個變量的不變性條件,其中涉及的全部變量都須要由同一個鎖來保護。

 

2.5活躍性與性能:

  將同步代碼塊分解得過細並很差,由於獲取與釋放鎖操做須要開銷。

  當執行時間較長的計算或者可能沒法快速完成的操做時(如I/O),必定不要持有鎖。

  同時使用兩種不一樣的同步機制會帶來混亂,在性能或安全性上也沒有任何好處。(如內置鎖synchronized和Atomic原子變量) 。

 

 

第3章 對象的共享

 

同步,並不只僅只包括原子性這一項內容,還有另一個很是重要的方面:內存可見性(Memory Visibility)。

3.1重排序:

  好比兩步賦值操做,賦值的順序可能會跟看到的順序相反。

  在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整。在缺少足夠同步的多線程程序中,要想對內存操做的執行順序進行判斷,幾乎沒法得出正確的結論。

失效數據:

  在沒有同步的狀況下,線程去讀取變量時,可能會獲得一個已經失效的值。更糟糕的是,失效值可能不會同時出現:一個線程可能得到某個變量的最新值,而得到另外一個變量的失效值。

最低安全性:

  當線程在沒有同步的狀況下讀取變量時,可能會獲得一個失效值,但至少這個值是由以前某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性(out-of-thin-airsafety)。

  最低安全性適用於絕大多數變量,當時存在一個例外:非volatile類型的64位數值變量。Java內存模型要求,變量的讀取操做和寫入操做都必須是原子操做,但對於非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位的操做。

加鎖和可見性

  加鎖的含義不只僅侷限於互斥行爲,還包括內存可見性。爲了確保全部線程都能看到共享變量的最新值,全部執行讀操做或者寫操做的線程都必須在同一個鎖上同步。

Volatile變量

  這是一種稍微弱一點的同步機制,主要就是用於將變量的更新操做通知到其它線程

  加鎖機制既能夠保證可見性又能夠保證原子性,而volatile變量只能確保可見性。

  可見性:在讀取volatile類型的變量時總會返回最新寫入的數據。

  禁止指令重排序:不會將該變量上的操做與其它內存操做一塊兒重排序。

  僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。

  典型用法:檢查某個狀態標記以判斷是否退出循環。

1 volatile boolean flag ;
2 while(!flag){
3      dosomething();
4 }

  volatile的語義不足以確保count++的原子性。

  當且僅當知足如下全部條件時,才應該使用volatile變量:

    對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
    該變量不會與其餘狀態變量一塊兒歸入不變性條件中。
    在訪問變量時不須要加鎖。

 

 3.2發佈

  是對象可以在當前做用域以外的代碼中使用。能夠是如下幾種狀況:

  ①將對象的引用保存到一個公有的靜態變量中,以便任何類和線程都能看到該對象;

  ②發佈某個對象的時候,在該對象的非私有域中引用的全部對象都會被髮布;

  ③發佈一個內部的類實例,內部類實例關聯一個外部類引用。

 

逸出:

  某個不該該發佈的對象被公佈的時候。某個對象逸出後,你必須假設有某個類或線程可能會誤用該對象,因此要封裝。

  不要在構造過程當中使this引用逸出。

  常見錯誤:在構造函數中啓動一個線程。

 

3.3線程封閉:

  當訪問共享的可變數據時,一般須要使用同步。一種避免同步的方式就是不共享數據。

  僅在單線程內訪問數據,就不須要同步,這種技術被稱爲線程封閉(Thread Confinement)。

  典型應用:①Swing的可視化組件和數據模型對象都不是線程安全的,Swing經過將它們封閉到Swing的實際分發線程中來實現線程安全;②JDBC的Connection對象。

線程封閉技術

  ①Ad-hoc線程封閉:維護線程封閉性的職責徹底由程序實現來承擔。

  在volatile變量上存在一個特殊的線程封閉:能確保只有單個線程對共享的volatile變量執行寫入操做(其餘線程有讀取volatile變量),那麼就能夠安全地在這些共享的volatile變量上執行「讀取-修改-寫入」的操做,而其餘讀取volatile變量的線程也能看到最新的值。

  ②棧封閉:在棧封閉中,只能經過局部變量才能訪問對象

  ③ThreadLocal類:這是線程封閉的更規範方法,這個類能使線程中的某個值與保存值的對象關聯起來

  提供get()和set()方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以get老是返回由當前執行線程在調用set時設置的最新值。

  ThreadLocal對象一般用於防止對可變的單實例變量(Singleton)或全局變量進行共享。怎麼理解呢?仍是JDBC的Connection對象,防止共享,因此一般將JDBC的鏈接保存到ThreadLocal對象中,每一個線程都會擁有屬於本身的鏈接。

 

3.4不變性:

  知足同步需求的另外一種方法是使用 不可變對象(Immutable Object)。

  不可變對象必定是線程安全的。

  當知足如下這些條件的時候,對象纔是不可變的:

    對象建立之後其狀態就不能被修改;

    對象的全部域都是 final 類型;

    對象是正確建立的(在對象的建立期間,this引用沒有逸出)。

  兩種很好的編程習慣:

    除非使用更高的可見性,不然應將全部的域都聲明爲私有的;

    除非須要某個域是可變的,不然都應該聲明爲final域;

 

3.5安全發佈的經常使用模式:

  由於可變對象必須以安全的方式發佈,這就意味着發佈和使用該對象的線程時都必須使用同步。

  要安全的發佈一個對象,對象的引用以及對象的狀態必須同時對其它線程可見。一個正確構造的對象能夠經過如下方式來安全地發佈:

    在靜態初始化函數中初始化一個對象引用。
    將對象的引用保存到volatile類型的於或者AtomicReferance對象中。
    將對象的引用保存到某個正確構造對象的final類型域中。
    將對象的引用保存到一個由鎖保護的域中。

安全地共享對象:

  在併發程序中使用和共享對象時,可使用一些實用的策略,包括:

    線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,而且只能由這個線程修改;

    只讀共享。在沒有額外的同步的狀況下,共享的只讀對象能夠由多個線程併發訪問,但任何線程都不能修改它。

    線程安全共享。線程安全的對象在其內部實現同步,所以多個線程能夠經過對象的共有接口來進行訪問而不須要進一步的同步。

    保護對象。被保護的對象只能經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及已發佈的而且由某個特定鎖保護的對象。

 

 

第5章 基礎構建模塊

 

5.1同步容器類:

  同步容器:能夠簡單地理解爲經過synchronized來實現同步的容器,好比Vector、Hashtable以及SynchronizedList等容器,若是有多個線程調用同步容器的方法,它們將會串行執行。

  線程安全實現的方式:將他們的可變成員變量封裝起來,並對每一個方法都進行同步,使得每次僅僅有一個線程能訪問這些可變的成員變量。

 

  儘管這些類的方法都是同步的,但當併發訪問多個方法的時候,仍是有可能出錯。

好比,有兩個線程,一個執行同步的get方法,一個執行同步的remove方法,那麼這兩個線程仍然可能出現競態條件(多個線程不一樣的時序致使程序出問題):「先remove在get就會出現問題」,在使用的時候仍然要注意。 

 

ConcurrentModificationException異常:

  若是有其餘線程併發的修改容器,就要在迭代期間對容器加鎖。

  當迭代器發現容器在迭代過程當中被修改時,就會拋出一個ConcurrentModificationException異常。

  具體過程:將計數器的變化與容器關聯起來,若是在迭代期間計數器被修改,那麼hasNext()和next()將拋出如上異常。

  若是在迭代期間不但願對容器加鎖,那麼一種替代方法就是「克隆」容器,並在副本上進行迭代。

 

隱藏迭代器:

  調用了vector的toString()函數,這個函數就是一個隱藏的迭代過程。若是在這個過程當中,一個線程得到了CPU而且執行了remove()方法,也會報告ConcurrentModificationException異常。

  hashCode和equals也會間接執行迭代操做。

  因此在全部對共享容器進行迭代的地方都要加鎖。

  

5.2併發容器:

  java 5.0提供了多種併發容器類來改進同步容器類的性能。同步容器將全部對容器狀態的訪問都串行化,以實現線程安全。嚴重下降併發性,當多個線程競爭容器的鎖時,吞吐量將嚴重下降。

  經過併發容器來代替同步容器,能夠極大的提升伸縮性而且下降安全性風險。 

 

ConcurrentHashMap:

  基於散列的Map,並非將每一個方法都在同一個鎖上同步使得每次只能有一個線程訪問線程,而使用一種更細粒度的加鎖機制來實現更大程度的共享。這種機制稱爲分段鎖(Lock Striping)。在這種機制中,任意數量的讀取線程能夠併發地訪問map執行讀取操做的線程和執行寫入操做的線程能夠併發地訪問map,而且必定數量的寫入線程能夠併發地修改map。ConcurrentHashMap帶來的結果是,在併發訪問的環境下將實現更高的吞吐量,而在單線程環境中只損失很是小的性能。

  ConcurrentHashMap與其餘併發容器一塊兒加強了同步容器類:它們提供的迭代器不會拋出ConcurrentModificationException,所以不須要在迭代過程當中對容器加鎖。ConcurrentHashMap返回的迭代器具備弱一致性。弱一致性的迭代器能夠容忍併發的修改,當建立迭代器時會遍歷已有的元素,並能夠(可是不保證)在迭代器被構造後將修改操做反應給容器。

  與 Hashtable 和 synchronizedMap 相比,ConcurrentHashMap 有更多的優點以及更少的劣勢。所以在大多數狀況下,用 ConcurrentHashMap 來代替同步 Map 能進一步提升代碼的可伸縮性。只有當應用程序須要加鎖Map 以進行獨佔訪問時,才能放棄使用 ConcurrentHashMap。

 

CopyOnWriteArrayList:

  用於替代同步List,在迭代期間不須要對容器進行加鎖或複製。(相似,CopyOnWriteArraySet代替同步set)。

「寫入時複製(Copy-On-Write)」容器的線程安全性在於,只要是正確發佈一個事實不可變的對象,那麼在訪問該對象時就再也不須要進一步的同步。在每次修改時,都會建立並從新發佈一個新的容器副本,從而實現可變性。

  每次修改容器都會複製底層數組,須要必定的開銷。僅當迭代的操做遠遠多於修改操做時,才應該使用「寫入時複製「的容器。

 

5.3阻塞隊列和生產者-消費者模式:

  BlockingQueue阻塞隊列提供可阻塞的put和take方法,以及支持定時的offer和poll方法。若是隊列已經滿了,那麼put方法將阻塞直到空間可用;若是隊列爲空,那麼take方法將阻塞直到有元素可用。

  隊列能夠是有界的也能夠是無界的。

  阻塞隊列提供了一個offer方法,若是數據項不能被添加到隊列中,那麼將返回一個失敗狀態。
  在構建高可靠的應用程序時,有界隊列是一種強大的資源管理工具:它們能抵制並防止產生過多的工做項,使應用程序在負荷過載的狀況下變得更加健壯。

 

BlockingQueue的多種實現: 
  LinkedBlockingQueueArrayBlockingQueue:是FIFO,兩者分別與LinkedList和ArrayList相似,但比同步List擁有更好的併發性能。 

  PriorityBlockingQueue:是一個按優先級排序的隊列,當你但願按照某種順序而不是FIFO來處理元素時,這個隊列將很是有用。

  SynchronousQueue:事實上它並非一個真正的隊列,由於它不會爲隊列中元素維護存儲空間。與其餘隊列不一樣的是,它維護一組線程,這些線程在等待這把元素加入或移出隊列。 
若是以洗盤子爲比喻,就至關於沒有盤架來暫時存放洗好的盤子,而是將洗好的盤子直接放入下一個空閒的烘乾機中。

 

串行線程封閉:

  優勢:對於可變對象,生產者-消費者這種設計與阻塞隊列一塊兒,促進了串行線程封閉,從而將對象全部權從生產者交付給消費者。

       線程封閉對象只能由單個線程擁有,經過安全地發佈該對象「轉移」全部權,實現了轉移前由前一線程獨佔,轉移後由後一線程獨佔。

  實現方法:阻塞隊列使得這種線程封閉的全部權轉移變得容易,其次還能夠經過ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet來完成這項工做。

 

雙端隊列與工做密取:

  Java 6增長了兩種容器類型,Deque和BlockingDeque這兩種容器類型,分別對Queue和BlockingQueue進行了擴展。

  Deque是一個雙端隊列,實現了在隊列頭和隊列尾的高效插入和移除。具體實現包括ArrayDeque和LinkedBlockingDeque。

  在生產者-消費者模式中,全部消費者有一個共享的工做隊列,而在工做密取設計中,每一個消費者都有各自的雙端隊列。若是一個消費者完成了本身雙端隊列中的所有工做,那麼它能夠從其餘消費者雙端隊列末尾祕密地獲取工做。

  密取工做模式比傳統的生產者-消費者模式具備更高的可伸縮性,這是由於工做者線程不會在單個共享的任務隊列上發生競爭。這是由於工做者線程不會在單個共享的任務隊列上發生競爭。在大多數狀況下,它們都只是訪問本身的雙端隊列,從而極大地減小了競爭。當工做者線程須要訪問另外一個隊列時,它會從隊列的尾部而不是頭部獲取工做,所以進一步下降了隊列上的競爭程度。

 

 

第6章 任務執行

  任務一般是一些抽象的且離散的工做單元。經過把應用程序的工做分解到多個任務中,能夠簡化程序的組織結構。

  好比頁面渲染器要執行繪製文本元素和繪製圖像的任務。

 

6.1在線程中執行任務:

  併發程序設計的第一步就是要劃分任務的邊界,理想狀況下就是全部的任務都獨立的:每一個任務都是不依賴於其餘任務的狀態,結果和邊界。由於獨立的任務是最有利於併發設計的。

  有一種最天然的任務劃分方法就是以獨立的客戶請求爲任務邊界。每一個用戶請求是獨立的,則處理任務請求的任務也是獨立的。

 

顯示地爲任務建立線程:

  任務處理線程從主線程分離出來,使得主線程不用等待任務完畢就能夠去快速地去響應下一個請求,以達到高響應速度;

  任務處理能夠並行,支持同時處理多個請求;

  任務處理是線程安全的,由於每一個任務都是獨立的。

 

無限制建立線程的不足:

  線程的生命週期的開銷很大:每建立一個線程都是要消耗大量的計算資源;

  資源的消耗:活躍的線程要消耗內存資源,若是有太多的空閒資源就會使得不少內存資源浪費,致使內存資源不足,多線程併發時就會出現資源強佔的問題;

  穩定性:可建立線程的個數是有限制的,過多的線程數會形成內存溢出;

 

6.2Executor框架:

  任務是一組邏輯工做單元,而線程則是任務異步執行的機制。爲了讓任務更好地分配到線程中執行,java.util.concurrent提供了Executor框架。

  Executor基於生產者-消費者模式:提交任務的操做至關於生產者(生成待完成的工做單元),執行任務的線程則至關於消費者(執行完這些工做單元)。

  經過使用Executor,將請求處理任務的提交與任務的實際執行解耦開來。

 

線程池:

  線程池從字面意思來看,是指管理一組同構工做線程的資源池。

  在線程池中執行任務比「爲每個任務分配一個線程」優點更多。經過重用現有的線程而不是建立新線程,能夠在處理多個請求時分攤在線程建立和銷燬過程當中產生的巨大開銷。

另一個額外的好處是,當請求到達時,工做線程一般已經存在,所以不會因爲等待建立線程而延遲任務的執行,從而提升了響應性。

 

Executors中的靜態工廠方法提供了一些線程池:

  newFixedThreadPool:固定長度的線程池

  newCachedThreadPool:可緩存的線程池,線程池的規模不存在限制

  newSingleThreadExecutor:單線程的線程池

  newScheduledThreadPool:固定長度,且以延遲或定時的方式來執行任務

 

Executor的生命週期:

ExecutorService提供了兩種方法關閉方法:

  shutdown: 平緩的關閉過程,即再也不接受新的任務,等到已提交的任務執行完畢後關閉進程池;

  shutdownNow: 馬上關閉全部任務,不管是否再執行;

 

延遲任務和週期性任務:

Java中提供Timer來執行延時任務和週期任務,可是Timer類有如下的缺陷:

  Timer只會建立一個線程來執行任務,若是有一個TimerTask執行時間太長,就會影響到其餘TimerTask的定時精度;

  Timer不會捕捉TimerTask未定義的異常,因此當有異常拋出到Timer中時,Timer就會崩潰,並且也沒法恢復,就會影響到已經被調度可是沒有執行的任務,形成「線程泄露」。

  建議使用ScheduledThreadPoolExecutor來代替Timer類。

 

6.3找出可利用的並行性:

  Executor以Runnable的形式描述任務,可是Runnable有很大的侷限性:

    沒有返回值,只是執行任務;

    不能處理被拋出的異常;

  爲了彌補以上的問題,Java中設計了另外一種接口Callable。

 

Callable:

  Callable支持任務有返回值,並支持異常的拋出。若是但願得到子線程的執行結果,那Callable將比Runnable更爲合適。

  不管是Callable仍是Runnable都是對於任務的抽象描述,即代表任務的範圍:有明確的起點,而且都會在必定條件下終止。

 

Executor框架下所執行的任務都有四種生命週期:

  建立;

  提交;

  開始;

  完成;

對於一個已提交但尚未開始的任務,是能夠隨時被中止;可是若是一個任務已經若是已經開始執行,就必須等到其相應中斷時再取消;固然,對於一個已經執行完成的任務,對其取消任務是沒有任何做用的。

 

Future:

  Future類表示任務生命週期狀態,並提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等,其命名體現了任務的生命週期只能向前不能後退。

  Future類提供方法查詢任務狀態外,還提供get方法得到任務的返回值,可是get方法的行爲取決於任務狀態:

    若是任務已經完成,get方法則會馬上返回;

    若是任務還在執行中,get方法則會擁塞直到任務完成;

    若是任務在執行的過程當中拋出異常,get方法會將該異常封裝爲ExecutionException中,並能夠經過getCase方法得到具體異常緣由;

  若是將一個Callable對象提交給ExecutorService,submit方法就會返回一個Future對象,經過這個Future對象就能夠在主線程中得到該任務的狀態,並得到返回值。

  除此以外,能夠顯式地把Runnable和Callable對象封裝成FutureTask對象,FutureTask不光繼承了Future接口,也繼承Runnable接口,因此能夠直接調用run方法執行。

 

CompletionService:

  CompletionService能夠理解爲Executor和BlockingQueue的組合:當一組任務被提交後,CompletionService將按照任務完成的順序將任務的Future對象放入隊列中。

  除了使用CompletionService來一個一個獲取完成任務的Future對象外,還能夠調用ExecutorSerive的invokeAll()方法。

  invokeAll支持限時提交一組任務(任務的集合),並得到一個Future數組。invokeAll方法將按照任務集合迭代器的順序將任務對應的Future對象放入數組中,這樣就能夠把傳入的任務(Callable)和結果(Future)聯繫起來。當所有任務執行完畢,或者超時,再或者被中斷時,invokeAll將返回Future數組。

  當invokeAll方法返回時,每一個任務要麼正常完成,要麼被取消,即都是終止的狀態了。

 

 

第8章 線程池的使用

8.1任務與執行策略之間的隱性耦合

  並不是全部的任務都能使用全部的執行策略。有些類型的任務須要明確地指定執行策略,包括: 

  依賴性任務:若是提交給線程池的任務須要依賴其餘的任務,那麼就隱含地給執行策略帶來了約束,此時必須當心地維持這些執行策略以免產生活躍性問題。

  使用線程封閉機制的任務:任務要求其執行所在的Executor是單線程的。若是將Executor從單線程環境改成線程池環境,那麼將失去線程安全性。

  對響應時間敏感的任務:若是將一個運行時間較長的任務提交到單線程的Executor中,或者將多個運行時間較長的任務提交到一個只包含少許線程的線程池中,那麼將下降由該Executor管理的服務的響應性。

  使用ThreadLocal的任務:只有當線程本地值的生命受限於任務的生命週期時,在線程池的線程中使用ThreadLocal纔有意義,而在線程池中不該該使用ThreadLocal在任務之間傳遞值。

 

線程飢餓死鎖:

  在線程中,若是任務依賴與其餘任務,那麼可能產生死鎖。 

  在單線程的Executor中,若是一個任務將另外一個任務提交到同一個Executor,而且等待這個被提交任務的結果,那麼一般會引起死鎖。第二個任務停留在工做隊列中,並等待第一個任務完成,而第一個任務又沒法完成,由於它在等待第二個任務的完成。

  在更大的線程池中,若是全部正在執行的任務的線程都因爲等待其餘仍處於工做隊列的任務而阻塞,那麼會發生一樣的問題,這個現象被稱爲線程飢餓死鎖(Thread Starvation Deadlock)

 

運行時間較長的任務:

  有限線程池線程可能會被執行時間長任務佔用過長時間,最終致使執行時間短的任務也被拉長了「執行」時間。能夠考慮限定任務等待資源的時間,而不要無限制地等待。

 

8.2設置線程池的大小

  線程池的理想大小取決於被提交任務的類型以及所部署系統的特性。在代碼中一般不會固定線程池的大小,而應該經過某種配置機制來提供,或者根據Runtime.availableProcessors來動態計算。

  在計算密集型的任務,在擁有Ncpu個處理器的系統上,當線程池的大小爲Ncpu+1,一般能實現最優的利用率。

  對於包含I/O操做或其餘阻塞操做的任務,因爲線程不會一直執行,所以線程池的規模應該更大、要正確地設置線程池的大小,你必須估算出任務的等待時間與計算時間的比值,這能夠經過一些分析或監控工具來得到。

  CPU週期並非惟一影響線程池大小的資源,還包括內存,文件句柄,套接字句柄和數據庫鏈接等。

 

8.3配置ThreadPoolExecutor

  ThreadPoolExecutor是一個靈活的,穩定的線程池,容許進行各類定製。

1 public ThreadPoolExecutor(int corePoolSize,
2    int maximumPoolSize,
3    long keepAliveTime,
4    TimeUnit unit,
5    BlockingQueue<Runnable> workQueue,
6    ThreadFactory threadFactory,
7    RejectedExecutionHandler handler) { ... }

 

  corePoolSize:基本大小也就是線程池的目標大小,即在沒有任務執行時(初期線程並不啓動,而是等到有任務提交時才啓動,除非調用prestartAllCoreThreads)線程池的大小,而且只有在工做隊列滿了的狀況下才會建立超出這個數量的線程。

  maximumPoolSize:線程池的最大大小表示可同時活動的線程數量的上限。若是某個線程的空閒時間超過了存活時間,那麼將被標記爲可回收的,而且當線程池的當前大小超過了基本大小時,這個線程將被終止。

  newFixedThreadPool:工廠方法將線程池的基本大小和最大大小設置爲參數中指定的值,並且建立的線程池不會超時。

  newCachedThreadPool:工廠方法將線程池的最大大小設置爲Integ.MAX_VALUE,並且將基本大小設置爲0,並將超時設置爲1分鐘,這種方法建立的線程池能夠被無限擴展,而且當需求下降時會自動收縮

 

執行excute方法:

  1 .若是當前運行的線程少於corePoolSize,則建立新線程來執行任務(須要得到全局鎖) 
  2 .若是運行的線程等於或多於corePoolSize ,則將任務加入BlockingQueue 
  3 .若是沒法將任務加入BlockingQueue(隊列已滿),則建立新的線程來處理任務(須要得到全局鎖) 
  4. 若是建立新線程將使當前運行的線程超出maxiumPoolSize,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()方法。

 

管理隊列任務:

  ThreadPoolExecutor容許提供一個BlockingQueue來保存等待執行的任務。 
  基本的任務排隊方法有3種:無界隊列(unbounded queue,),有界隊列(bounded queue,)和同步移交(synchronous handoff)。 

  newFixedThreadPoolnewSingleThreadExecutor在默認狀況下將使用一個無界的LinkedBlockingQueue

  若是全部工做者線程都處於忙碌狀態,那麼任務將在隊列中等候。若是任務持續地達到,而且超過了線程池處理它們的速度,那麼隊列將無限制地增長。  

 

  一種更穩妥的資源管理策略是使用有界隊列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。 

  有界隊列有助於避免資源耗盡的狀況發生,但隊列填滿後,由飽和策略解決。

 

 

  在newCachedThreadPool工廠方法中使用了SynchronousQueue。 

  對於很是大的或者無界的線程池,能夠經過使用SynchronousQueue來避免任務排隊,以及直接將任務從生產者移交給工做者線程。 

  SynchronousQueue不是一個真正的隊列,而是一種在線程之間移交的機制。 

  要將一個元素放入SynchronousQueue中,必須有另外一個線程正在等待接受這個元素。若是沒有線程正在等待,而且線程池的當前大小小於最大值,那麼TrheadPoolExecutor將建立一個新的線程,不然根據飽和策略,這個任務將被拒絕。  使用直接移交將更高效,由於任務會直接移交給執行它的線程,而不是首先放在隊列中,而後由工做者線程從隊列中提取該任務。 

  只有當線程池是無界的或者能夠拒絕任務時,SynchronousQueue纔有實際價值。

 

  對於Executor,newCachedThreadPool工廠方法是一種很好的默認選擇,他能提供比固定大小的線程更好的排隊性能(因爲使用了SynchronousQueue而不是LinkedBlockingQueue)。
  當須要限制當前任務的數量以知足資源管理需求時,能夠選擇固定大小的線程池,就像在接受網絡用戶請求的服務器應用程序中,若是不進行限制,容易發生過載問題。

 

飽和策略:

  當有界隊列被填滿後,飽和策略開始發揮做用。 
  ThreadPoolExecutor的飽和策略能夠經過調用setRejectedExecutionHandler來修改。(若是某個任務被提交到一個已被關閉的Executor時,也會用到飽和策略) 

 

  AbortPolicy:「停止(Abort)策略」是默認的飽和策略,該策略將拋出未檢查的Rejected-ExecutionException。調用這能夠捕獲這個異常,而後根據需求編寫本身的處理代碼。

  DiscardPolicy:「拋棄(Discard)策略」會拋棄超出隊列的任務。

  DiscardOldestPolicy:「拋棄最舊策略「則會拋棄下個將被執行任務,而後嘗試從新提交新的任務。(若是工做隊列是一個優先隊列,那麼拋棄最舊策略將致使拋棄優先級最高的任務,所以最好不要將拋棄最舊飽和策略和優先級隊列一塊兒使用)  

  CallerRunsPolicy:「調用者運行策略「實現了一種調節機制。該策略不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而減低新任務的流量。  它不會在線程池的某個線程中執行新提交的任務,新任務會在調用execute時在主線程中執行。

 

線程工廠:

  每當線程池須要建立一個線程時,都是經過線程工廠方法來完成的。

  默認的線程工廠方法將建立一個新的,非守護的線程,而且不包含特殊的配置信息。 
  經過指定一個線程工廠方法,能夠定製線程池的配置信息。

  在ThreadFactory中只定義了一個方法newThread,每當線程池須要建立一個新線程都會調用這個方法。

 

在調用構造函數後再定製ThreadPoolExecutor:

  在調用完ThreadPoolExecutor的構造函數後,仍然能夠經過設置函數(Setter)來修改大多數傳遞給它的構造函數的參數(例如線程池的基本大小,最大大小,存活時間,線程工廠以及拒絕執行處理器(rejected execution handler))。若是Executor是經過Executors中的某個(newSingleThreadExecutor除外)工廠方法建立的,那麼能夠將結果的類型轉換爲ThreadPoolExecutor以訪問設置器。

 

8.4擴展ThreadPoolExecutor

  ThreadPoolExecutor是可擴展的,它提供了幾個能夠在子類化中改寫的方法:beforeExecute,afterExecute和terminated,這些方法能夠用於擴展ThreadPoolExecutor的行爲。 在執行任務的線程中將調用beforeExecute和afterExecute等方法,在這些方法中還能夠添加日誌,計時,監視或統計信息收集的功能。 

  不管是從run中正常返回,仍是拋出一個異常而返回,afterExecute都會被調用。(若是任務在完成後帶有一個Error,那麼就不會調用afterExecute)若是beforeExecute拋出一個RuntimeException,那麼任務將不被執行,而且afterExecute也不會被調用。

  在線程池完成關閉時調用terminated,也就是在全部任務都已經完成而且全部工做者線程也已經關閉後。terminated能夠用來釋放Executor在其生命週期裏分配的各類資源,此外還能夠執行發送通知,記錄日誌或收集finalize統計信息等操做。

 

8.5 遞歸算法的並行化

  若是在循環中包含了一些密集計算,或者須要執行可能阻塞的I/O操做,那麼只要每次迭代是獨立的,均可以對其進行並行化。

 1 void processSequentially(List<Element> elements) {  //串行
 2   for (Element e : elements)
 3   process(e);
 4 }
 5 void processInParallel(Executor exec, List<Element> elements) { //並行
 6   for (final Element e : elements)
 7   exec.execute(new Runnable() {
 8   public void run() { process(e); }
 9   });
10 }

 

 

第10章 避免活躍性危險

10.1 死鎖

  當一個線程永遠地持有一個鎖,而且其餘線程都嘗試得到這個鎖,那麼它們將永遠被阻塞。

  在線程A持有鎖L並想得到鎖M的同時,線程B持有鎖M並嘗試得到鎖L,那麼這兩個線程將永遠等待下去。這種狀況就是最簡單的死鎖形式

 

數據庫中監測死鎖以及從死鎖中恢復:

  當檢測到了一組事務發生死鎖時(經過在表示等待關係的有向圖中搜索循環),將選擇一個犧牲者並放棄這個事務。

 

  若是全部線程都按照固有的順序來獲取鎖,那麼在程序中就不會出現鎖順序死鎖的問題

  若是在持有鎖時調用某個外部方法,那麼將出現活躍性問題,在這個外部方法中有可能會獲取其餘鎖(這時有可能出現像以前的順序死鎖現象),或者阻塞時間過長,致使其餘線程沒法及時得到當前被持有的鎖

 

開放調用:

  相對於持有鎖時調用外部方法的狀況,若是在調用某個方法時不須要持有鎖,這種調用就叫作「開放調用」

  在程序中應該儘可能使用開放調用,以便於對依賴於開放調用的程序進行死鎖分析

  開放調用可能會使某個原子操做變成非原子操做,有時非原子操做也是能夠接受的

 

 1 class CooperatingNoDeadlock {
 2     class Taxi {
 3         private Point location, destination;
 4         private final Dispatcher dispatcher;
 5         public Taxi(Dispatcher dispatcher) {
 6             this.dispatcher = dispatcher;
 7         }
 8         public synchronized Point getLocation() {
 9             return location;
10         }
11         public  void setLocation(Point location) {
12             boolean reachedDestination;
13             synchronized (this) { 14                 this.location = location; 15                 reachedDestination = location.equals(destination); 16  } 17             if (reachedDestination) 18                 dispatcher.notifyAvailable(this); 19         }
20     }
21     class Dispatcher {
22         private final Set<Taxi> taxis;
23         private final Set<Taxi> availableTaxis;
24         public Dispatcher() {
25             taxis = new HashSet<Taxi>();
26             availableTaxis = new HashSet<Taxi>();
27         }
28         public synchronized void notifyAvailable(Taxi taxi) {
29             availableTaxis.add(taxi);
30         }
31         public Image getImage() {
32             Set<Taxi> copy;
33             synchronized (this) { 34                 copy = new HashSet<Taxi>(taxis); 35  } 36             Image image = new Image(); 37             for (Taxi t : copy) 38  image.drawMarker(t.getLocation()); 39             return image;
40         }
41     }
42 }

 

 

 

資源死鎖:

  相對於多個線程相互持有彼此正在等待的鎖而又不釋放本身持有的鎖時會發生死鎖,當這種等待發生在相同的資源集合上時,也會發生死鎖,稱之爲資源死鎖

 

10.2 死鎖的避免與診斷

  支持定時的鎖:就是顯式使用Lock類中的定時tryLock功能來代替內置鎖機制從而能夠檢測死鎖和從死鎖中回覆過來

 

10.3 死鎖其餘活躍性危險

  飢餓:當線程因爲沒法訪問它所須要的資源而不能繼續執行時,就發生了「飢餓」

  而引起線程飢餓最多見的資源就是CPU時鐘週期,若是一個線程的優先級不當或者在持有鎖時發生無限循環、無限等待某個資源,這就會致使此線程長期佔用CPU時鐘週期,其餘須要這個鎖的線程沒法獲得這個鎖,所以就發生了飢餓

  避免使用優先級,由於這會增長平臺依賴性從而致使活躍性問題,多數狀況下,使用默認的線程優先級就能夠了

 

  活鎖:這種問題發生時,儘管不會阻塞線程,但也不能繼續執行,由於線程將不斷重複相同的操做,並且老是失敗

  要解決活鎖問題,須要在重試機制中引入隨機性(如以太協議在重複發生衝突時採用指數方式回退機制:衝突發生時等待隨機的時間而後重試,若是等待的時間相同的話仍是會衝突)

  在併發應用程序中,經過等待隨機長度的時間和回退能夠有效地避免活鎖的發生。

 

 

第13章 顯示鎖

13.1 Lock與ReentrantLock

  Lock接口中定義了一種無條件、可輪詢的、定時的以及可中斷的鎖獲取操做,全部加鎖和解鎖的方法都是顯式的。

  ReentrantLock實現了Lock接口,提供了與synchronized一樣的互斥性和可見性,也一樣提供了可重入性。

  unlock必須在finally中釋放鎖,不然可能出現死鎖。

public interfece Lock
{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long timeout, TimeUnit unit 
        throw InterruptedException;
    void unlock();
    Condition newCondition();
}

 

輪詢鎖與定時鎖:

  可由tryLock來實現

  能夠避免死鎖的發生

  輪詢鎖經過釋放已得到的鎖,並退回從新嘗試獲取全部鎖(lock.tryLock())

  定時鎖經過釋放已得到的鎖,放棄本次操做(lock.tryLock(timeout, unit))來避免死鎖

 

可中斷的鎖獲取操做:

  Lock.lockInterruptibly():用該方法獲取鎖,能夠響應中斷

  若是線程未被中斷,也不能獲取到鎖,就會一直阻塞下去,直到獲取到鎖或發生中斷請求

  定時的lock.tryLock(timeout, unit)一樣能響應中斷

 

非塊結構加鎖:

  內置鎖是基於塊結構的加鎖

  Lock可使塊與塊交叉實現非塊結構的加鎖(連鎖式加鎖或者鎖耦合),例:鏈表中,next節點加鎖後,釋放pre節點的鎖

 

13.3 公平性

  公平鎖——Lock fairLock = new ReentrantLock(true); 

  公平鎖:線程將按照它們發出請求的順序來得到鎖

  非公平鎖:當一個線程請求非公平的鎖時,若是在發出請求的同時該鎖的狀態變爲可用,那麼這個線程將跳過隊列中全部的等待線程並得到這個鎖

 

  公平性將因爲在掛起線程和恢復線程時存在的開銷而極大地下降性能(非公平性的鎖容許線程在其餘線程的恢復階段進入加鎖代碼塊)

  當持有鎖的時間相對較長,或者請求鎖的平局時間間隔較長,那麼應該使用公平鎖

  內置鎖爲非公平鎖

 

13.4 選擇

  在一些內置鎖沒法知足需求的狀況下,ReentrantLock能夠做爲一種高級工具。

  當須要一些高級功能時才應該使用ReentrantLock,這些功能包括:可定時的、可輪詢的與可中斷的鎖獲取操做,公平隊列,以及非塊結構的鎖。

  不然,仍是應該優先使用synchronized

  synchronized,在線程轉儲中能給出在哪些調用幀中得到了哪些鎖

 

13.5 讀寫鎖

  對於在多處理器系統上被頻繁讀取的數據結構,讀 - 寫鎖可以提升性能。而在其餘狀況下,讀 - 寫鎖的性能比獨佔鎖的性能要略差一些,這是由於它們的複雜性更高

相關文章
相關標籤/搜索