從前幾章的學習當中,咱們知道了volidate只能保證可見性以及部分的原子性,而針對大部分的併發場景而言,部分的原子性是知足不了項目需求的,所以使用了鎖機制或者原子類操做來知足咱們的開發需求。java
在Java提供的鎖中,主要有Synchronized以及ReetrantLock類。在Java1.5以前,Synchronized並非同步最好的選擇,因爲併發時頻繁的阻塞和喚醒線程,致使頻繁的進行線程上下文切換,會形成Synhronized的併發效率在某些狀況下是遠不及ReentrantLock。而在Java1.6後,官方就對Synchronized進行了許多優化,極大的提升了Synchronized的性能。因此只要Synchronized能知足使用環境,建議使用Synchronized而不使用ReentrantLock。下面鎖優化就針對Synchronized而言來進行討論。git
首先的明白一個問題,爲何須要進行鎖優化呢?這個須要從線程的開銷和阻塞講起了。github
若是應用程序或者服務是單線程模型,即主線程是惟一的線程,即不存在同步的開銷,也不須要額外的同步操做。而在多線程中,在CPU調度切換不一樣線程時候會發生上下文切換,上下文切換時候,JVM須要去保存當前線程對應的寄存器使用狀態,以及代碼執行的位置等等,那麼確定是會有必定的開銷的。並且當線程因爲等待某個鎖而被阻塞的時候,JVM一般將該線程掛起,掛起線程和恢復線程都是須要轉到內核態中進行,頻繁的進行用戶態到內核態的切換對於操做系統的併發性能來講會形成不小的壓力。所以如何去優化Synchronized形成阻塞的性能開銷,就是JVM進行鎖優化的過程。算法
Synchronized其實是一種悲觀的策略,這也是咱們爲何稱Synchronized爲悲觀鎖的緣由,即老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。相比悲觀鎖,CAS技術則是一種樂觀鎖的概念,即老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。編程
從Java1.5到Java1.6,HotSpot團隊實現了大量的鎖優化的技術,其中包括自旋鎖,輕量級鎖和偏向鎖等等,下面就以左邊闡述的優化技術作個整理概括。安全
自旋鎖的原理比較簡單,就是若是共享的資源鎖定的狀態持續時間很短,那麼爲了這段時間去掛起和恢復線程並不值得,咱們只須要讓後面請求鎖的線程等一等(自旋),可是不放棄CPU的執行時間,等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的性能消耗了。多線程
自旋等待自己雖然避免了線程上下文切換的開銷,可是因爲不放棄CPU的執行時間,若是鎖佔用的時間很短,自旋的效果確定就好,若是鎖佔用的時間長了,那麼,這鎖佔用的時間內,CPU的消耗也會跟着增長往上漲。因此自旋等待須要有必定的時間限制,若是自旋超過了限定次數仍然獲取鎖失敗,那麼就恢復到傳統掛起阻塞的狀態執行。Java1.6中引入了自適應的自旋鎖概念,意味着自適應的時間再也不固定,JVM內部會經過必定的算法優化對應鎖的自旋次數,或者優化掉自旋過程,一切由虛擬機決定。併發
JDK1.6中-XX:+UseSpinning開啓;app
-XX:PreBlockSpin=10 爲自旋次數;jvm
JDK1.7後,去掉此參數,由jvm控制;
偏向鎖的目的在於消除數據在無競爭的狀況下的同步原語,偏向鎖會偏向於第一個得到它的線程,若是在接下來的運行過程該鎖沒有被其餘線程獲取,則持有偏向鎖的線程永遠不須要再進行同步;若是有另外一個線程嘗試獲取這個鎖的時候,偏向模式就宣告結束了。
偏向鎖能夠提升帶有同步可是五競爭的程序的性能,若是程序中大多數鎖老是被多個不一樣線程訪問,那麼偏向鎖就是多餘的了,所以是否使用偏向鎖能夠由開發者自主決定,使用-XX:+UseBiasedLocking
開啓,使用-XX:-UseBiaseLocking
來禁止偏向鎖優化。
輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。使用輕量級鎖可以同步性能的一句是「對於絕大部分的鎖沒在真個同步週期內都是不存在競爭的」。若是沒有競爭,輕量級鎖基於CAS執行,也就是避免了使用互斥操做的開銷;可是若是存在競爭,那麼除了互斥量的開銷,還額外發生CAS開銷,因此在存在競爭的條件下,輕量級鎖會比傳統的重量級鎖更慢。
Sychronized的內部執行以下,摘自該篇文章:
synchronized的執行過程:
須要注意,鎖只能依照偏向鎖->輕量級鎖->重量鎖升序升級,不可以降序。
上述爲JVM內部優化的策略,咱們須要理解便可,下面是咱們可以在代碼層次上對Synchronized使用進行優化的,如鎖消除,鎖粗化這兩個概念。
鎖消除是指JVM在編譯器運行期,對一些代碼要求同步,可是JVM檢測不可能形成共享數據競爭的鎖的優化消除技術。舉個例子:
public String getStrings(String s1, String s2, String s3) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); stringBuffer.append(s3); return stringBuffer.toString(); }
若是上述代碼運行在多線程環境,因爲該方法在多線程中不涉及到共享變量的問題,而StringBuffer自己內部保證線程安全性而加了Synchronized,因此在編譯器運行時,就會進行鎖消除的操做,上述代碼會忽略掉全部的同步而直接執行。
鎖粗化表示擴大鎖的同步範圍。通常而言咱們是儘可能縮小同步塊的範圍,旨在減小沒必要要的開銷,可是若是存在一系列的操做都對同一個對象反覆加鎖和解鎖,那麼即便沒有線程的競爭,頻繁的進行互斥同步操做也會致使沒必要要的性能損耗。如以下代碼:
public void loop() { int k = 0; for (int i = 0; i < 100; i++) { synchronized (User.class) { k++; } } System.out.println(k); }
在循環體內每一次循環都伴隨着加鎖解鎖的過程,勢必伴隨着沒必要要的性能消耗,上面的例子咱們能夠把Synchronized提到外面,只進行一次加鎖解鎖的操做:
public void loop() { int k = 0; synchronized (User.class) { for (int i = 0; i < 100; i++) { k++; } } System.out.println(k); }
經過使用Synchronized進行同步的操做,從代碼實操來看,確實是簡單易行的,而後從虛擬機的角度來看,使用Synchronized意味這使用阻塞同步操做,那麼上述也講了阻塞同步會形成線程的掛起和從新喚起的過程,即便上面講述了JVM層對Synchronizd的優化,卻仍然不能化解阻塞帶來的性能損耗。那麼有沒有一種非阻塞的同步操做呢?答案是有的,就是CAS,這也是Java中原子類可以表現出原子操做的根基。
首先介紹一下什麼是非阻塞同步:
非阻塞同步是基於衝突檢測的樂觀併發策略,即先進行操做,若是沒有其餘線程爭用共享數據,那麼操做成功;若是有共享數據爭用的狀況,而且產生了衝突,那麼採起其餘的補償策略(如不斷嘗試,知道成功)。非阻塞同步是不須要把線程掛起的。
非阻塞同步的關鍵在於如何保證檢測衝突的原子性,Java線程的帶來的問題與內存模型(JMM)說過JMM在交互協議中定義了8種操做爲原子性的,除此以外在計算機發展直到今日,現代廣泛的處理器都支持一些複雜的指令成爲原子性操做,常見的有:
這裏咱們就主要研究CAS,CAS操做包含了三個操做數---須要讀寫的內存位置V,進行比較的值A和要寫入的新值B。當且僅當V的值等於A時候,CAS纔會經過原子的方式更新B來更新值,不然不會執行任何操做。不管位置V的值是否等於A,都將返回V原有的值。一個CAS實現的類比代碼以下:
class FakeCAS { private int value; //保證原子性 public int getValue() { return value; } //保證原子性 public int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (oldValue == expectedValue) { value = newValue; } return oldValue; } //保證原子性 public boolean compareAndSet(int expectedValue, int newValue) { return (expectedValue == compareAndSwap(expectedValue, newValue)); } }
上面getValue()
和 compareAndSwap(...)
都表示爲原子性操做,這樣就實現了CAS的語義了。那麼一個簡單的基於CAS的自增操做以下操做便可:
class CASCount { private FakeCAS mFakeCAS; public int increment() { int v; do { v = mFakeCAS.getValue(); } while (v != mFakeCAS.compareAndSwap(v, v + 1)); return v + 1; } }
在一個無限循環中不斷的將值加1,若是失敗,說明已經有線程進行修改了,須要從新遍歷,直到循環退出爲止,當前狀況下,因爲沒有使用鎖操做,因此是不會阻塞的。
那麼CAS有沒問題呢?考慮到這樣的一個狀況,若是變量V初次讀取時候等於A,而且在準備賦值時候仍然等於A,可是在這期間有線程將值改成B後再改成A,那麼CAS就認爲V重來沒有變過,該問題被稱爲"ABA"問題。解決方案也有,就是Java中提供了AtomStampReference來保證處理ABA問題。不過通常狀況下ABA問題不影響程序的併發正確性,因此使用依照開發者決定。
Ok,CAS就介紹到這裏,有興趣的同窗能夠研究一下AtomInteger的實現,你能夠發現實現的思路跟上面的基本一致,至於如何保證CAS的原子性操做,Java層面調用了native的代碼,底層代碼在IA64,x86中使用cmpxchg實現,其餘處理器也有對應的指令保證,因爲對這方面的研究頗少,因此就不解析了。
Java中除了Synchronized可以進行加鎖的操做外,還提供了Lock接口實現類實現加鎖功能,在併發容器內常常可以看到的一個Lock實現類ReentrantLock。這裏不介紹ReentrantLock的使用方法,而是針對ReentrantLock中的公平鎖以及非公平鎖概念作個總結。
ReentrantLock中實現公平鎖和非公平鎖經過兩個內部類:FairSync和NoFairSync,這兩個類同事繼承自Sync類,其內部維護的一個雙向鏈表,表結點Node的值就是每個請求當前鎖的線程。
公平鎖的實現邏輯在於每次都是依次從隊首取值,也就是說按照隊列的順序依次執行對應線程,公平鎖的好處是等待鎖的線程不會餓死,可是總體效率相對低一些。
非公平鎖的實現邏輯跟公平鎖基本上是一致的,可是鎖是能夠搶佔的。噹噹前的鎖被某個線程佔用的時候,其餘線程競爭所資源時會添加到隊列當中,這時候跟公平鎖沒有其餘的區別;若是恰巧在某個時刻有線程須要獲取鎖,而這個時候恰好鎖可用,那麼這個線程會直接搶佔,而這時阻塞在等待隊列的線程則不會被喚醒。非公平鎖的好處是總體效率相對高一些,可是有些線程可能會餓死或者說很早就在等待鎖,但要等好久纔會得到鎖。