1.1什麼是鎖?html
在計算機科學中,鎖(lock)或互斥(mutex)是一種同步機制,用於在有許多執行線程的環境中強制對資源的訪問限制。鎖旨在強制實施互斥排他、併發控制策略。java
鎖一般須要硬件支持纔能有效實施。這種支持一般採起一個或多個原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap"」。這些指令容許單個進程測試鎖是否空閒,若是空閒,則經過單個原子操做獲取鎖。redis
1.2.鎖的一個重要屬性 粒度數據庫
在引入鎖粒度以前,須要瞭解關於鎖的三個概念:緩存
一、鎖開銷 lock overhead 鎖佔用內存空間、 cpu初始化和銷燬鎖、獲取和釋放鎖的時間。程序使用的鎖越多,相應的鎖開銷越大多線程
二、鎖競爭 lock contention 一個進程或線程試圖獲取另外一個進程或線程持有的鎖,就會發生鎖競爭。鎖粒度越小,發生鎖競爭的可能性就越小併發
三、死鎖 deadlock 至少兩個任務中的每個都等待另外一個任務持有的鎖的狀況鎖粒度是衡量鎖保護的數據量大小,一般選擇粗粒度的鎖(鎖的數量少,每一個鎖保護大量的數據),在當單進程訪問受保護的數據時鎖開銷小,可是當多個進程同時訪問時性能不好。由於增大了鎖的競爭。相反,使用細粒度的鎖(鎖數量多,每一個鎖保護少許的數據)增長了鎖的開銷可是減小了鎖競爭。例如數據庫中,鎖的粒度有表鎖、頁鎖、行鎖、字段鎖、字段的一部分鎖分佈式
相關術語 Critical Section(臨界區)、 Mutex/mutual exclusion(互斥體)、 Semaphore/binary semaphore(信號量)memcached
2.鎖的種類函數
2.1.獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個線程所持有。 (ReentrantLock、 Synchronized)
共享鎖是指該鎖可被多個線程所持有。 (ReadWriteLock)
互斥鎖/讀寫鎖
獨享鎖/共享鎖這是廣義上的說法,互斥鎖/讀寫鎖就分別對應具體的實現。在Java中如ReentrantLock就是互斥鎖(獨享鎖), ReadWriteLock就是讀寫鎖( 讀鎖是共享鎖,寫鎖是獨享鎖)。 獨享鎖與共享鎖也是經過AQS來實現的。
鎖升級:讀鎖到寫鎖 (不支持)
鎖降級:寫鎖到讀鎖 (支持)
2.2.讀寫鎖 ReentrantReadWriteLock
高16位表明寫鎖,低16位表明讀鎖
2.2.公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能會形成飢餓現象。
對於Java ReentrantLock和而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。
對於Synchronized而言,也是一種非公平鎖。因爲其並不像ReentrantLock是經過AQS的控制線程對鎖的獲取, 因此並無任何辦法使其變成公平鎖。
2.3.可重入鎖
可重入鎖又名遞歸鎖,是指同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。
ReentrantLock、ReadWriteLock和Synchronized都是可重入鎖。可重入鎖的一個好處是可必定程度避免死鎖
如上面的代碼,若是synchronized不是可重入鎖的話,testB就不會被當前線程執行,從而造成死鎖。
須要注意的是,可重入鎖加鎖和解鎖的次數要相等。
C==0代表未得到鎖,Else表示已經得到鎖,這時對state加1,相應的,每次釋放鎖都會對state減1
2.4.樂觀鎖/悲觀鎖
樂觀鎖/悲觀鎖不是指具體類型的鎖,而是看待併發的角度。
悲觀鎖認爲存在不少併發更新操做,採起加鎖操做,若是不加鎖必定會有問題
樂觀鎖認爲不存在不少的併發更新操做,不須要加鎖。數據庫中樂觀鎖的實現通常採用版本號,Java中可以使用CAS實現樂觀鎖。
cas無鎖機制:即compare and swap 或者 compare and set,涉及到三個操做數,數據所在的內存值,預期值,新值。當須要更新時,判斷當前內存值與以前取到的值是否相等,若相等,則用新值更新,若失敗則重試,通常狀況下是一個自旋操做,即不斷的重試。
關於cas具體可參考什麼是cas機制?
2.5.分段鎖
分段鎖是一種鎖的設計,並非一種具體的鎖。對於ConcuttentHashMap就是經過分段鎖實現高效的併發操做。
2.6.自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會阻塞,而是採用循環的方式嘗試獲取鎖。好處是減小上下文切換,缺點是一直佔用CPU資源。
2.7.偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,而且是針對Synchronized
。在Java 5經過引入鎖升級的機制來實現高效Synchronized
。這三種鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
下面是詳解
jdk1.6中對Synchronized鎖作的優化,首先了解下對象頭(Mark Word):
運行時JVM內存佈局
Mark Word在不一樣鎖狀態下的標誌位存儲
從jdk1.6開始爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。鎖共有四種狀態,級別從低到高分別是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。隨着競爭狀況鎖狀態逐漸升級、鎖能夠升級但不能降級。
偏向鎖的獲取和撤銷
HotSpot做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入偏向鎖。
線程1檢查對象頭中的Mark Word中是否存儲了線程1,若是沒有則CAS操做將Mark Word中的線程ID替換爲線程1。此時,鎖偏向線程1,後面該線程進入同步塊時不須要進行CAS操做,只須要簡單的測試一下Mark Word中是否存儲指向當前線程的偏向鎖,若是成功代表該線程已經得到鎖。若是失敗,則再須要測試一下Mark Word中偏向鎖標識是否設置爲1(是不是偏向鎖),若是沒有設置,則使用CAS競爭鎖,若是設置了,則嘗試使用CAS將偏向鎖指向當前線程
偏向鎖的競爭結果:
根據持有偏向鎖的線程是否存活
1.若是不活動,偏向鎖撤銷到無鎖狀態,再偏向到其餘線程
2.若是線程仍然活着,則升級到輕量級鎖
偏向鎖在Java6和Java7中默認是開啓的,可是在應用程序啓動幾秒後才激活,若是有必要能夠關閉延遲:
-XX:BiasedLockingStartupDelay=0
若是肯定應用程序中全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖:
-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖。
-XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking
輕量級鎖膨脹:
1.線程在執行同步塊以前,JVM會在當前棧楨中建立用於存儲鎖記錄的空間(Lock record),並將對象頭中的Mark Word複製到鎖記錄中(Displaced Mark Word)。
2.而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針
3.若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程嘗試使用自旋來獲取鎖
偏向鎖、輕量級鎖、重量級鎖的優缺點
1.偏向鎖是爲了不某個線程反覆得到/釋放同一把鎖時的性能消耗,若是仍然是同個線程去得到這個鎖,嘗試偏向鎖時會直接進入同步塊,不須要再次得到鎖。
2.而輕量級鎖和自旋鎖都是爲了不直接調用操做系統層面的互斥操做,由於掛起線程是一個很耗資源的操做。
爲了儘可能避免使用重量級鎖(操做系統層面的互斥),首先會嘗試輕量級鎖,輕量級鎖會嘗試使用CAS操做來得到鎖,若是輕量級鎖得到失敗,說明存在競爭。可是也許很快就能得到鎖,就會嘗試自旋鎖,將線程作幾個空循環,每次循環時都不斷嘗試得到鎖。若是自旋鎖也失敗,那麼只能升級成重量級鎖。
3.可見偏向鎖,輕量級鎖,自旋鎖都是樂觀鎖。
逃逸分析:
逃逸分析:通俗一點講,當一個對象的指針被多個方法或線程引用時,咱們稱這個指針發生了逃逸,必須在JIT裏完成
若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,則能夠爲這個變量進行一些高效的優化:
從jdk1.6開始默認開啓:
開啓: -XX:+DoEscapeAnalysis
關閉: -XX:-DoEscapeAnalysis
鎖粗化:
若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把加鎖同步的範圍擴展到整個操做序列的外部,這樣就只須要加鎖一次就夠了
3.1.Synchronized與ReentrantLock的區別
ReentrantLock = 一個AQS同步器(維護同步狀態) + 一個AQS同步隊列 + 多個Condition等待隊列
3.2 ReentrantLock繼承體系類圖
ReentrantLock#lock()方法時序圖
以上內容大部分轉自http://www.javashuo.com/article/p-stjweyux-nh.html
4 分佈式鎖
目前幾乎不少大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。
在不少場景中,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案
分佈式鎖應該具有的條件
1、在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行; 2、高可用的獲取鎖與釋放鎖; 3、高性能的獲取鎖與釋放鎖; 4、具有可重入特性; 5、具有鎖失效機制,防止死鎖; 六、具有非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗
針對分佈式鎖的實現,目前比較經常使用的有如下幾種方案:
基於數據庫實現分佈式鎖
基於緩存(Redis,memcached,tair)實現分佈式鎖
基於Zookeeper實現分佈式鎖
實現方式參考分佈式鎖的簡單入門以及三種實現方式介紹
java分佈式看這篇就夠了(redis實現分佈式鎖的不一樣方式)
關於redis開源分佈式鎖redisson原理介紹參考