Java並行程序基礎 --- 多線程及容錯性處理

1. Java中提供的多線程編程手段

1.1 JDK5以前

  • 只有synchronized一種

1.2 JDK5到如今:

  • Atomic
  • Lock
  • AtomicStampedReference
  • ReentrantLock

2 多線程編程容易產生的問題

2.1 共享狀態

你們都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU比起來要慢的多,所以若是任什麼時候間對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在CPU裏面就有了高速緩存html

也就是,當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,好比下面的這段代碼:編程

i=i+1

當線程執行這個語句時,會先從主存當中讀取i的值,而後複製一份到高速緩存當中,而後CPU執行指令對i進行加1操做,而後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。緩存

這個代碼在單線程中運行是沒有任何問題的,可是在多線程中運行就會有問題了。在多核CPU中,每條線程能夠運行於不一樣的CPU中,所以每一個線程運行時有本身的高速緩存(對單核CPU來講,其實也會出現這種問題。只不過是以線程調度的形式來分別執行的)。本文咱們以多核CU爲例。安全

下圖中,線程1和線程2共享i,並同時對i進行i++操做,可能存在這種狀況,最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。多線程

爲了解決緩存不一致問題,一般來講有如下2種解決方法:框架

  • 經過在總線加Lock#鎖的方式
  • 經過緩存一致性協議

這兩種方式都是硬件層面上提供的方式。性能

在早期的CPU當中,是經過在總線加Lock#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加Lock#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只有一個CPU能使用這個變量的內存。好比上面例子中,若是一個線程在執行i=i+1過程當中,在總線上發出了LOCK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從變量i所在的內存讀取變量。這樣就解決了緩存不一致的問題。優化

可是上面的方式會有一個問題,因爲在鎖定總線期間,其餘CPU沒法訪問內存,容易致使如下的Starvation(飢餓)和DeadLock(死鎖)問題。spa

因此就出現了緩存一致性協議。最出名的就是Intel的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它的核心思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存中從新讀取。線程

2.2 Starvation(飢餓)

  • 高優先級的線程可能會佔用全部CPU資源使得低優先級線程沒法運行
  • 線程可能由於其餘線程的競爭而沒法進入一個同步塊
  • Notify方法喚醒的老是其餘線程以致於等待線程老是在sleep狀態

2.3 DeadLock(死鎖)

線程1鎖定A,爾後嘗試鎖定B,線程2鎖定B,爾後嘗試鎖定A,形成兩個線程同時等待對方釋放鎖,進入死鎖狀態。

總結:多線程編程容易產生問題的根本緣由在於鎖

3. 鎖的不一樣類型(詳情請看《鎖優化的思路和方法》的第二部分)

3.1 偏向鎖

3.2 輕量級鎖

3.3 自旋鎖(Spinlock)

不用切換至內核態,可能會更高效

3.4 互斥鎖(Mutex)或信號量

不佔用CPU資源
會切換至內核態

4. Volatile

  • 一個線程寫,另一個線程讀取,能夠保證共享變量的可見性
  • 當兩個線程同時讀寫共享變量,Volatile無效
  • Volatile的讀寫會引發緩存內容失效,形成每一次讀寫均須要從主內存讀取。所以性能會受到影響

4.1 正確使用volatile

你只能在有限的一些情形下使用volatile變量替代鎖。要是volatile變量提供理想的線程安全,必須同時知足下面兩個條件:

  • 對變量的寫操做不依賴於當前值。
  • 對變量沒有包含在具備其餘變量的不變式中。

實際上,這些條件代表,volatile變量不能用做線程安全計數器。雖然增量操做(x++)看起來相似一個單獨操做,實際上它是一個有讀取-修改-寫入操做序列組成的組合操做必須以原子方式執行,而volatile不能提供必須的原子性操做。

簡單局其中使用一例:狀態標誌

也許實現volatile變量的規範使用僅僅是使用一個boolean狀態標誌,用於指示發生了一個重要的一次性時間,例如完成初始化或請求停機。

不少應用程序包含了一種控制結構,形式爲"在尚未準備好中止程序時再執行一些工做"。如如下代碼所示,將volatile變量做爲狀態標誌使用:

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

極可能會從循環外部調用shutdown()方法 -- 即在另外一個線程中 -- 所以,須要執行某種同步來確保正確實現shutdownRequested變量的可見性。相對於synchronized,這裏很是適合使用volatile。

5. 互斥鎖和信號量

  • Java中的互斥鎖是synchronized或者Lock,而信號量是Semaphore。
  • 互斥鎖能夠看做是信號量的一個特例:能夠把互斥鎖比做家裏的衛生間,每次只能一我的進入,信號量比做是公共衛生間,有幾個隔間就能夠進入。
  • Synchronized是reentrant(可重入)的,即兩個對同一個變量synchronized的方法a、b,一個線程能夠在a內進入b。
  • Lock不是reentrant的,若是須要reentrancy,使用ReentrantLock。
  • Semaphore沒有reentrancy的概念,Semaphore只關心還有多少空間,不論當前線程是否已經佔有了空間,只要當前線程進入Semaphore,就會形成Semaphore的計數-1。
  •  
  • Java除有鎖編程以外,同時支持原子變量,即本人另外一篇博客《Java並行程序基礎 --- 無鎖》的內容。
  • 原子變量是無鎖編程,能夠看做是volatile的擴展
  • 原子變量在volatile的基礎上,保證了對CAS(compare and swap)操做的支持,容許多個線程對同一個原子變量讀寫操做。
  • 原子變量不能保證ABA問題,即一個變量的原始值爲A,被修改成B,後又被修改成A,這時其餘線程沒法得知當前的A實際上是通過兩次修改後的值
  • 對於ABA問題,能夠加入版本號,經過AtomicStampedReference支持。

總結:

  • 根據前面講述的多線程編程技巧,咱們可以經過無鎖編程解決共享變量的問題,可是無鎖編程須要比有鎖編程更高的技巧,使得程序更加複雜,同時一旦發生多線程引發的問題,更加難以排查。
  • 而有鎖編程引入了deadlocl和starvation的問題

那麼如何解決starvation的問題?

引入公平鎖

  • 因爲公平鎖加入了更加複雜的上鎖機制,以及保證了每次喚醒的均是不一樣的線程,因此公平鎖的效率比普通的鎖更低(可是會高於直接的synchronized)。
  • 經過公平鎖能夠避免因爲線程始終不被喚醒形成的starvation,可是沒法避免其餘兩種狀況形成的starvation。
  • 因此最好的多線程編程,實際上是不上鎖,而兩個高效的使用多線程的處理框架Disruptor以及Akka,均採用了不上鎖的方式,儘管方法不一樣。
     

參考:http://www.importnew.com/18126.html

參考:http://www.cnblogs.com/shangxiaofei/p/5564047.html

參考:煉數成金--多線程及容錯性處理

相關文章
相關標籤/搜索