這篇文章分爲六個部分,不一樣特性的鎖分類,併發鎖的不一樣設計,Synchronized中的鎖升級,ReentrantLock和ReadWriteLock的應用,幫助你梳理 Java 併發鎖及相關的操做。java
通常咱們提到的鎖有如下這些:算法
上面是不少鎖的名詞,這些分類並非全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面分別說明。數據庫
樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不一樣角度,在Java和數據庫中都有此概念對應的實際應用。編程
顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。數組
樂觀鎖適用於多讀的應用類型,樂觀鎖在Java中是經過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操做就經過CAS自旋實現的。緩存
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是經過CAS來實現了樂觀鎖。安全
簡單來講,CAS算法有3個三個操做數:數據結構
當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然返回V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認爲在它修改以前,必定會有其它線程去修改它,悲觀鎖效率很低。多線程
老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。架構
傳統的MySQL關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身。
公平鎖的優勢是等待鎖的線程不會餓死。缺點是總體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程之外的全部線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。
非公平鎖的優勢是能夠減小喚起線程的開銷,總體的吞吐效率高,由於線程有概率不阻塞直接得到鎖,CPU沒必要喚醒全部線程。缺點是處於等待隊列中的線程可能會餓死,或者等好久纔會得到鎖。
java jdk併發包中的ReentrantLock能夠指定構造函數的boolean類型來建立公平鎖和非公平鎖(默認),好比:公平鎖可使用new ReentrantLock(true)實現。
是指該鎖一次只能被一個線程所持有。
是指該鎖可被多個線程所持有。
對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
抽象隊列同步器(AbstractQueuedSynchronizer,簡稱AQS)是用來構建鎖或者其餘同步組件的基礎框架,它使用一個整型的volatile變量(命名爲state)來維護同步狀態,經過內置的FIFO隊列來完成資源獲取線程的排隊工做。
concurrent包的實現結構如上圖所示,AQS、非阻塞數據結構和原子變量類等基礎類都是基於volatile變量的讀/寫和CAS實現,而像Lock、同步器、阻塞隊列、Executor和併發容器等高層類又是基於基礎類實現。
相交進程之間的關係主要有兩種,同步與互斥。所謂互斥,是指散佈在不一樣進程之間的若干程序片段,當某個進程運行其中一個程序片斷時,其它進程就不能運行它們之中的任一程序片斷,只能等到該進程運行完這個程序片斷後才能夠運行。所謂同步,是指散佈在不一樣進程之間的若干程序片段,它們的運行必須嚴格按照規定的某種前後次序來運行,這種前後次序依賴於要完成的特定的任務。
顯然,同步是一種更爲複雜的互斥,而互斥是一種特殊的同步。
也就是說互斥是兩個線程之間不能夠同時運行,他們會相互排斥,必須等待一個線程運行完畢,另外一個才能運行,而同步也是不能同時運行,但他是必需要安照某種次序來運行相應的線程(也是一種互斥)!
總結:互斥:是指某一資源同時只容許一個訪問者對其進行訪問,具備惟一性和排它性。但互斥沒法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數狀況),經過其它機制實現訪問者對資源的有序訪問。在大多數狀況下,同步已經實現了互斥,特別是全部寫入資源的狀況一定是互斥的。少數狀況是指能夠容許多個訪問者同時訪問資源。
在訪問共享資源以前對進行加鎖操做,在訪問完成以後進行解鎖操做。 加鎖後,任何其餘試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。
若是解鎖時有一個以上的線程阻塞,那麼全部該鎖上的線程都被編程就緒狀態, 第一個變爲就緒狀態的線程又執行加鎖操做,那麼其餘的線程又會進入等待。 在這種方式下,只有一個線程可以訪問被互斥鎖保護的資源
這個時候讀寫鎖就應運而生了,讀寫鎖是一種通用技術,並非Java特有的。
讀寫鎖特色:
互斥鎖特色:
Linux內核也支持讀寫鎖。
互斥鎖 pthread_mutex_init() pthread_mutex_lock() pthread_mutex_unlock() 讀寫鎖 pthread_rwlock_init() pthread_rwlock_rdlock() pthread_rwlock_wrlock() pthread_rwlock_unlock() 條件變量 pthread_cond_init() pthread_cond_wait() pthread_cond_signal()
自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。
在Java中,自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。
典型的自旋鎖實現的例子,能夠參考自旋鎖的實現
它是爲實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較相似,它們都是爲了解決對某項資源的互斥使用。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,也就說,在任什麼時候刻最多隻能有一個執行單元得到鎖。可是二者在調度機制上略有不一樣。對於互斥鎖,若是資源已經被佔用,資源申請者只能進入睡眠狀態。
可是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,」自旋」一詞就是所以而得名。
下面是個簡單的例子:
public class SpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); public void lock() { Thread current = Thread.currentThread(); // 利用CAS while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread current = Thread.currentThread(); cas.compareAndSet(current, null); } }
lock()方法利用的CAS,當第一個線程A獲取鎖的時候,可以成功獲取到,不會進入while循環,若是此時線程A沒有釋放鎖,另外一個線程B又來獲取鎖,此時因爲不知足CAS,因此就會進入while循環,不斷判斷是否知足CAS,直到A線程調用unlock方法釋放了該鎖。
根據所鎖的設計方式和應用,有分段鎖,讀寫鎖等。
分段鎖實際上是一種鎖的設計,並非具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。
以ConcurrentHashMap來講一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
可是,在統計size的時候,可就是獲取hashmap全局信息的時候,就須要獲取全部的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。
鎖消除,如無必要,不要使用鎖。Java 虛擬機也能夠根據逃逸分析判斷出加鎖的代碼是否線程安全,若是確認線程安全虛擬機會進行鎖消除提升效率。
鎖粗化。若是一段代碼須要使用多個鎖,建議使用一把範圍更大的鎖來提升執行效率。Java 虛擬機也會進行優化,若是發現同一個對象鎖有一系列的加鎖解鎖操做,虛擬機會進行鎖粗化來下降鎖的耗時。
輪詢鎖是經過線程不斷嘗試獲取鎖來實現的,能夠避免發生死鎖,能夠更好地處理錯誤場景。Java 中能夠經過調用鎖的 tryLock 方法來進行輪詢。tryLock 方法還提供了一種支持定時的實現,能夠經過參數指定獲取鎖的等待時間。若是能夠當即獲取鎖那就當即返回,不然等待一段時間後返回。
讀寫鎖 ReadWriteLock 能夠優雅地實現對資源的訪問控制,具體實現爲 ReentrantReadWriteLock。讀寫鎖提供了讀鎖和寫鎖兩把鎖,在讀數據時使用讀鎖,在寫數據時使用寫鎖。
讀寫鎖容許有多個讀操做同時進行,但只容許有一個寫操做執行。若是寫鎖沒有加鎖,則讀鎖不會阻塞,不然須要等待寫入完成。
ReadWriteLock lock = new ReentrantReadWriteLock(); Lock readLock = lock.readLock(); Lock writeLock = lock.writeLock();
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。
在 Java 6 以前,Monitor 的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
四種狀態會隨着競爭的狀況逐漸升級,並且是不可逆的過程,即不可降級。
這四種狀態都不是Java語言中的鎖,而是Jvm爲了提升鎖的獲取與釋放效率而作的優化(使用synchronized時)。
這三種鎖是指鎖的狀態,而且是針對Synchronized。在Java 5經過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。
當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操做(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖。這樣作的假設是基於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。
若是有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就須要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。
ReentrantLock,一個可重入的互斥鎖,它具備與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。
public class LockTest { private Lock lock = new ReentrantLock(); public void testMethod() { lock.lock(); for (int i = 0; i < 5; i++) { System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1))); } lock.unlock(); } }
synchronized與wait()和nitofy()/notifyAll()方法相結合能夠實現等待/通知模型,ReentrantLock一樣能夠,可是須要藉助Condition,且Condition有更好的靈活性,具體體如今:
在併發場景中用於解決線程安全的問題,咱們幾乎會高頻率的使用到獨佔式鎖,一般使用java提供的關鍵字synchronized(關於synchronized能夠看這篇文章)或者concurrents包中實現了Lock接口的ReentrantLock。
它們都是獨佔式獲取鎖,也就是在同一時刻只有一個線程可以獲取鎖。而在一些業務場景中,大部分只是讀數據,寫數據不多,若是僅僅是讀數據的話並不會影響數據正確性(出現髒讀),而若是在這種業務場景下,依然使用獨佔鎖的話,很顯然這將是出現性能瓶頸的地方。
針對這種讀多寫少的狀況,java還提供了另一個實現Lock接口的ReentrantReadWriteLock(讀寫鎖)。讀寫所容許同一時刻被多個讀線程訪問,可是在寫線程訪問時,全部的讀線程和其餘的寫線程都會被阻塞。
ReadWriteLock,顧明思義,讀寫鎖在讀的時候,上讀鎖,在寫的時候,上寫鎖,這樣就很巧妙的解決synchronized的一個性能問題:讀與讀之間互斥。
ReadWriteLock也是一個接口,原型以下:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
該接口只有兩個方法,讀鎖和寫鎖。
也就是說,咱們在寫文件的時候,能夠將讀和寫分開,分紅2個鎖來分配給線程,從而能夠作到讀和讀互不影響,讀和寫互斥,寫和寫互斥,提升讀寫文件的效率。
下面的實例參考《Java併發編程的藝術》,使用讀寫鎖實現一個緩存。
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Cache { static Map<String,Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); static Lock readLock = readWriteLock.readLock(); static Lock writeLock = readWriteLock.writeLock(); public static final Object getByKey(String key){ readLock.lock(); try{ return map.get(key); }finally{ readLock.unlock(); } } public static final Object getMap(){ readLock.lock(); try{ return map; }finally{ readLock.unlock(); } } public static final Object put(String key,Object value){ writeLock.lock(); try{ return map.put(key, value); }finally{ writeLock.unlock(); } } public static final Object remove(String key){ writeLock.lock(); try{ return map.remove(key); }finally{ writeLock.unlock(); } } public static final void clear(){ writeLock.lock(); try{ map.clear(); }finally{ writeLock.unlock(); } } public static void main(String[] args) { List<Thread> threadList = new ArrayList<Thread>(); for(int i =0;i<6;i++){ Thread thread = new PutThread(); threadList.add(thread); } for(Thread thread : threadList){ thread.start(); } put("ji","ji"); System.out.println(getMap()); } private static class PutThread extends Thread{ public void run(){ put(Thread.currentThread().getName(),Thread.currentThread().getName()); } } }
讀寫鎖支持鎖降級,遵循按照獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖可以降級成爲讀鎖,不支持鎖升級,關於鎖降級下面的示例代碼摘自ReentrantWriteReadLock源碼中:
void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // 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(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } } }
關注公衆號:架構進化論,得到第一手的技術資訊和原創文章