樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。java
樂觀鎖認爲對於同一個數據的併發操做,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷從新的方式更新數據。樂觀的認爲,不加鎖的併發操做是沒有事情的。算法
樂觀鎖在Java中的使用,是無鎖編程,經常採用的是CAS算法,典型的例子就是原子類,經過CAS自旋實現原子操做的更新。編程
悲觀鎖認爲對於同一個數據的併發操做,必定是會發生修改的,哪怕沒有修改,也會認爲修改。所以對於同一個數據的併發操做,悲觀鎖採起加鎖的形式。悲觀的認爲,不加鎖的併發操做必定會出問題。數組
悲觀鎖適合寫操做很是多的場景,樂觀鎖適合讀操做很是多的場景,不加鎖會帶來大量的性能提高。緩存
分段鎖實際上是一種鎖的設計,並非具體的一種鎖。多線程
咱們以ConcurrentHashMap
來講一下分段鎖的含義以及設計思想,ConcurrentHashMap
中的分段鎖稱爲Segment,它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。併發
分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。函數
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。性能
非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會形成優先級反轉或者飢餓現象。優化
對於Java ReentrantLock
而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。
對於Synchronized
而言,也是一種非公平鎖。因爲其並不像ReentrantLock
是經過AQS的來實現線程調度,因此並無任何辦法使其變成公平鎖。
不可重入鎖,即若當前線程執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。必須先釋放鎖,才能從新獲取。
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。
對於Java ReentrantLock
而言, 他的名字就能夠看出是一個可重入鎖,其名字是Re entrant Lock
從新進入鎖。
對於Synchronized
而言,也是一個可重入鎖。可重入鎖的一個好處是可必定程度避免死鎖。
獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
對於Java ReentrantLock
而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock
,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
Synchronized也是獨享鎖。
獨享鎖/共享鎖這是廣義上的說法,互斥鎖/讀寫鎖就分別對應具體的實現。
Java中的具體實現ReentrantLock、Synchronized。
java中具體實現ReentrantReadWriteLock,容許多個讀線程同時訪問,但不容許寫線程和讀線程、寫線程和寫線程同時訪問。讀寫鎖內部維護了兩個鎖,一個用於讀操做,一個用於寫操做。
ReentrantReadWriteLock支持如下功能:
1)支持公平和非公平的獲取鎖的方式;
2)支持可重入。讀線程在獲取了讀鎖後還能夠獲取讀鎖;寫線程在獲取了寫鎖以後既能夠再次獲取寫鎖又能夠獲取讀鎖;
3)還容許從寫入鎖降級爲讀取鎖,其實現方式是:先獲取寫入鎖,而後獲取讀取鎖,最後釋放寫入鎖。可是,從讀取鎖升級到寫入鎖是不容許的;
4)讀取鎖和寫入鎖都支持鎖獲取期間的中斷;
5)Condition支持,僅寫入鎖提供一個 Conditon 實現;讀取鎖不支持 Conditon,readLock().newCondition() 拋出 UnsupportedOperationException。
使用
示例一:利用重入來執行升級緩存後的鎖降級。
class CachedData { Object data; volatile boolean cacheValid; //緩存是否有效 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); //獲取讀鎖 //若是緩存無效,更新cache;不然直接使用data if (!cacheValid) { // Must release read lock before acquiring write lock //獲取寫鎖前須釋放讀鎖 rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock //鎖降級,在釋放寫鎖前獲取讀鎖 rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); //釋放讀鎖 } }
示例二:使用 ReentrantReadWriteLock 來提升 Collection 的併發性
class RWDictionary { private final Map<String, Data> m = new TreeMap<String, Data>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); //讀鎖 private final Lock w = rwl.writeLock(); //寫鎖 public Data get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); } } public Data put(String key, Data value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } } }
實現原理
ReentrantReadWriteLock含有兩把鎖readerLock和writerLock,其中ReadLock和WriteLock都是內部類。
ReentrantReadWriteLock 也是基於AQS實現的,它的自定義同步器(繼承AQS)須要在同步狀態(一個整型變量state)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫鎖實現的關鍵。若是在一個整型變量上維護多種狀態,就必定須要「按位切割使用」這個變量,讀寫鎖將變量切分紅了兩個部分,高16位表示讀,低16位表示寫。
這裏不分析具體如何獲取鎖和釋放鎖。
阻塞的代價
java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在用戶態與內核態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。
自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖。
自旋鎖的原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
可是線程自旋是須要消耗cup的,說白了就是讓cup在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用cup自旋作無用功,因此須要設定一個自旋等待的最大時間。若是自旋超過最大時間仍然沒法獲取到鎖,這時線程會中止自旋進入阻塞狀態。
優缺點:
優勢:自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換。
缺點:若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用cpu作無用功。
因此自旋鎖適合競爭不是很激烈且執行的同步塊時間較短的狀況下。
這三種鎖是指鎖的狀態或階段,而且都是針對Synchronized
的。從jdk1.6開始爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。
Synchronized
鎖共有四種狀態,級別從低到高分別是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。隨着競爭狀況鎖狀態逐漸升級、鎖能夠升級但不能降級。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,再有另外一個線程訪問時,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程過來獲取鎖,此時鎖被佔用該線程自旋獲取鎖,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
鎖的實現:
ReenTrantLock的實現是一種自旋鎖,經過循環調用CAS操做來實現加鎖。它的性能比較好也是由於避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是咱們去分析和理解鎖設計的關鍵鑰匙。
Synchronized原始的Synchronized是依賴於JVM實現的,性能比較差,但自JDK1.6以後引入了偏向鎖、輕量級鎖,同時也借鑑了ReentrantLock的CAS思想實現加鎖,優化以後的性能已經和ReentrantLock基本相同。官方甚至建議使用synchronized。
區別:
便利性:很明顯Synchronized的使用比較方便簡潔,而且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock須要手工聲明來加鎖和釋放鎖,爲了不忘記手工釋放鎖形成死鎖,因此最好在finally中聲明釋放鎖。
鎖的細粒度和靈活度:很明顯ReenTrantLock優於Synchronized。
ReenTrantLock獨有能力:
除了須要用到以上三點功能時其餘都推薦使用synchronized方式加鎖。