我們知道面向物件的編程思想是站在現實世界的角度去抽象和解決問題,它把資料和行為都看做是物件的一部分,當多個執行緒訪問一個物件時如果不考慮這些執行緒在執行時環境下的調度和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那么這個物件就是執行緒安全的
Java的執行緒安全
我們這里討論的執行緒安全,限定于多個執行緒之間存在共享資料訪問這個前提,因為如果一段代碼根本不會與其他執行緒共享資料,那么從執行緒安全的角度來看,程式是串行執行還是多執行緒執行對它來說是完全沒有區別的,
我們可以將Java語言中各種操作共享的資料分為以下5類:不可變、絕對執行緒安全、相對執行緒安全、執行緒兼容和執行緒對立,
不可變
在Java語言中不可變的物件一定是執行緒安全的,無論是物件的方法實作還是方的呼叫者,都不需要再采用任何的執行緒安全保障措施,
- 如final關鍵字修飾的變數,只要一個不可變物件被正確構建出來(沒有發生this參考逃逸,即物件還未構造完成this參考就被發布出去了),那其外部的可見狀態永遠不會發生改變,永遠不會看到它在多個執行緒之中處于不一致的狀態,
- 如java.lang.String類的物件,它是一個典型的不可變物件,我們呼叫它substring、replace和concat這些方法都不會影響它原來的值,只會回傳一個新構造的字串物件
- 列舉型別,以及java.lang.Number的部分子類,如Long和Double等數值包裝型別,BigInteger和BigDecimal等大資料型別基本都是不可變的API;但同為Number的子型別的原子類AtomicInteger和AtomicLong則并非不可變的
這里理解一點,就是不可變的物件一定是執行緒安全的,
絕對執行緒安全
不管運行時環境如何,呼叫者都不需要任何額外的同步措施,java.util.Vector是一個執行緒安全的容器,它的get,add,size等方法都是被synchronized修飾的,效率很低 ,但確實是安全的,但并不意味著呼叫時永遠不需要同步手段,如多執行緒中一個執行緒在錯誤的時間里對元素進行了洗掉,就會導致邊界例外,此時洗掉操作需要進入同步塊處理,
相對執行緒安全
相對的執行緒安全就是我們通常意義上所講的執行緒安全,要保證對這個物件單獨的操作是執行緒安全的,呼叫時不需要欄位外的保障措施,但對一些特定的順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性, Java中大部分都屬于這類,如Vector,HashTable,Collections的synchronizedCollection()方法包裝的集合等,
執行緒兼容
執行緒兼容是指物件本身并不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在并發環境中可以安全地使用,我們平常說一個類不是執行緒安全的,絕大多數時候指的是這一種情況,Java API中大部分的類都是屬于執行緒兼容的,如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等,
執行緒對立
執行緒對立是指無論呼叫端是否采取了同步措施,都無法在多執行緒環境中并發使用的代碼,由于Java語言天生就具備多執行緒特性,執行緒對立這種排斥多執行緒的代碼是很少出現的,而且通常都是有害的,應當盡量避免,
執行緒安全的討論范疇
我們最終的目的就是使執行緒安全,其實從并發的角度來講,按照執行緒安全的三種策略:
- 第一個部分,阻塞(互斥)同步,我們所討論的鎖也集中在這個部分,
- 第二個部分,非阻塞同步,這個部分也就一種通過CAS進行原子類操作,其實也就是不加鎖或者代碼實作一些自旋鎖,
- 第三個部分,無同步方案,包括可重入代碼和執行緒本地存盤(ThreadLocal)
我們使用最多的應該就是虛擬機提供的互斥同步和鎖機制,互斥同步是常見的一種并發正確性保障手段,同步是指在多執行緒并發訪問共享資料時,保證共享資料在同一時刻只能被一個執行緒使用,而互斥是實作同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實作方式,互斥是因,同步是果;互斥是方法,同步是目的
Java的鎖
Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率,我們先來看看鎖的實作方式及其分類,
鎖的實作方式
我們所說的鎖的分類其實應該按照鎖的特性和設計來劃分,其實我們真正用到的鎖也就那么兩三種,只不過依據設計方案和性質對其進行了大量的劃分,一類是原生語意上的實作
- Synchronized,它是一個:非公平,悲觀,獨享,互斥,可重入的重量級鎖
還有一類是在JUC包下,是API層面上的實作
- ReentrantLock,它是一個:默認非公平但可實作公平的,悲觀,獨享,互斥,可重入的重量級鎖,
- ReentrantReadWriteLocK,它是一個,默認非公平但可實作公平的,悲觀,寫獨享/讀共享,讀寫,可重入的重量級鎖
那么我們來詳細了解下這幾種實作方式,
Synchronized
synchronized關鍵字經過編譯之后,會在同步塊的前后分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件,
- 如果Java程式中的synchronized明確指定了物件引數,那就是這個物件的reference;
- 如果synchronized修飾的是實體方法,去取對應的物件實體
- 如果synchronized修飾的是類方法,去取對應的Class物件來作為鎖物件
那么Synchronized實作的鎖有什么優缺點呢?
Synchronized優點
在虛擬機規范對monitorenter和monitorexit的行為描述中,有兩點是需要特別注意的,
- 首先,synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題, 可重?鎖概念是:??可以再次獲取??的內部鎖,?如?個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重?的話,就會造成死鎖,同?個執行緒每次獲取鎖,鎖的計數器都?增1,所以要等到鎖的計數器下降為0時才能釋放鎖
- 其次,同步塊在已進入的執行緒執行完之前,會阻塞后面其他執行緒的進入
也就是Synchronized能保證同步塊內的內容在多執行緒下準確執行
Synchronized缺點與優化
其缺點也比較明顯,Java的執行緒是映射到作業系統的原生執行緒之上的,如果要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要從用戶態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間,所以synchronized是Java語言中一個重量級的操作,優化方式就是在通知作業系統阻塞執行緒之前加入一段自旋等待程序,避免頻繁地切入到核心態之中,也就是在直接用重量級鎖之前,先讓輕量級鎖自旋等待下,
ReentrantLock
除了synchronized之外,我們還可以使用java.util.concurrent(下文稱JUC)包中的重入鎖ReentrantLock來實作同步,ReentrantLock與synchronized很相似,他們都具備一樣的執行緒重入特性,只是代碼寫法上有點區別,
- 一個表現為API層面的互斥鎖(lock和unlock方法配合try/finally陳述句塊來完成)
- 一個表現為原生語法層面的互斥鎖,
相比synchronized,ReentrantLock增加了一些高級功能,主要有以下3項:定時鎖等候/等待可中斷、可實作公平鎖,以及鎖可以系結多個條件
定時鎖等候/等待可中斷
ReentrantLock獲取鎖定有四種方式:
- lock(), 如果獲取了鎖立即回傳,如果別的執行緒持有鎖,當前執行緒則一直處于休眠狀態,直到獲取鎖
- tryLock(), 如果獲取了鎖立即回傳true,如果別的執行緒正持有鎖,立即回傳false
- tryLock(long timeout,TimeUnit unit), 如果獲取了鎖定立即回傳true,如果別的執行緒正持有鎖,會等待引數給定的時間,在等待的程序中,如果獲取了鎖定,就回傳true,如果等待超時,回傳false;定時鎖等候
- lockInterruptibly():如果獲取了鎖定立即回傳,如果沒有獲取鎖定,當前執行緒處于休眠狀態,直到獲取鎖定,或者當前執行緒被別的執行緒中斷,中斷鎖等候
可中斷特性對處理執行時間非常長的同步塊很有幫助,舉例說明,執行緒A和B都要獲取物件O的鎖定,假設A獲取了物件O鎖,B將等待A釋放對O的鎖定
- 如果使用 synchronized ,如果A不釋放,B將一直等下去,不能被中斷
- 如果 使用ReentrantLock,如果A不釋放,可以使B在等待了足夠長的時間以后,中斷等待,而干別的事情
所以這個特性還是很重要的,
可實作公平鎖
公平鎖是指多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖,synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的,但可以通過帶布林值的建構式要求使用公平鎖
鎖系結多個條件
鎖系結多個條件是指一個ReentrantLock物件可以同時系結多個Condition物件,而在synchronized中,鎖物件的wait和notify或notifyAll方法可以實作一個隱含的條件,如果要和多于一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這樣做,只需要多次呼叫newCondition方法即可,
Synchronized和ReentrantLock區別
Synchronized和ReentrantLock有什么區別和聯系呢?,可以總結為以下幾點:
- 兩者默認都是非公平,悲觀,獨享,互斥,可重入鎖
- synchronized 依賴于 JVM ? ReentrantLock 依賴于 API,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現例外,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實作的,要保證鎖定一定會被釋放,就必須將unLock放到finally{}中
- ReentrantLock 擁有Synchronized相同的并發性和記憶體語意,此外還多了 可實作選擇性通知(鎖可以系結多個條件),定時鎖等候/等待可中斷,可實作公平鎖
需要注意,隨著Synchronized的優化,性能已不能作為二者比較的標準,
鎖的分類
了解了具體的鎖的實作之后我們再來看看從功能的角度出發,鎖是如何進行分類的

樂觀鎖和悲觀鎖
樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待執行緒同步的不同角度,對于同一個資料的并發操作,
- 悲觀鎖認為自己在使用資料的時候一定有別的執行緒來修改資料,因此在獲取資料的時候會先加鎖,確保資料不會被別的執行緒修改,Java中,synchronized關鍵字和Lock的實作類都是悲觀鎖,
- 樂觀鎖認為自己在使用資料時不會有別的執行緒修改資料,所以不會添加鎖,只是在更新資料的時候去判斷之前有沒有別的執行緒更新了這個資料,如果這個資料沒有被更新,當前執行緒將自己修改的資料成功寫入,如果資料已經被其他執行緒更新,則根據不同的實作方式執行不同的操作(例如報錯或者自動重試),
悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,樂觀鎖在Java中是通過使用無鎖編程來實作,最常采用的是CAS演算法,Java原子類中的遞增操作就通過CAS自旋實作的

樂觀鎖的實作方式
樂觀鎖的實作方式主要有兩種,一種是CAS(Compare and Swap,比較并交換)機制,一種是版本號機制,
- CAS機制,CAS操作包括了三個運算元,分別是需要讀取的記憶體位置(V)、進行比較的預期值(A)和擬寫入的新值(B),操作邏輯是,如果記憶體位置V的值等于預期值A,則將該位置更新為新值B,否則不進行操作,另外,許多CAS操作都是自旋的,意思就是,如果操作不成功,就會一直重試,直到操作成功為止,
- 版本號機制,版本號機制的基本思路,是在資料中增加一個version欄位用來表示該資料的版本號,每當資料被修改版本號就會加1,當某個執行緒查詢資料的時候,會將該資料的版本號一起讀取出來,之后在該執行緒需要更新該資料的時候,就將之前讀取的版本號與當前版本號進行比較,如果一致,則執行操作,如果不一致,則放棄操作,
CAS指令需要有3個運算元,分別是記憶體位置(在Java中可以簡單理解為變數的記憶體地址,用V表示)、舊的預期值(用A表示)和新值(用B表示),CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會回傳V的舊值,上述的處理程序是一個原子操作,
悲觀鎖的實作方式
悲觀鎖的實作方式也就是加鎖,加鎖既可以在代碼層面(比如Java中的synchronized關鍵字),也可以在資料庫層面(比如MySQL中的排他鎖)
樂觀鎖的問題
CAS雖然很高效,但是它也存在三大問題,這里簡單說一下:
- ABA問題,CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值,但是如果記憶體值原來是A,后來變成了B,然后又變成了A,那么CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的,ABA問題的解決思路就是在變數前面添加版本號,每次變數更新的時候都把版本號加一,這樣變化程序就從
A-B-A變成了1A-2B-3A,JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中,compareAndSet()首先檢查當前參考和當前標志與預期參考和預期標志是否相等,如果都相等,則以原子方式將參考值和標志的值設定為給定的更新值, - 回圈時間長開銷大,CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷,
- 只能保證一個共享變數的原子操作,對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的,
Java從1.5開始JDK提供了AtomicReference類來保證參考物件之間的原子性,可以把多個變數放在一個物件里來進行CAS操作,但是還是沒有使用synchronized方便
二者的適用場景
樂觀鎖與悲觀鎖相比,適用的場景受到了更多的限制,無論是CAS機制還是版本號機制,
- 樂觀鎖的功能有限,比如,CAS機制只能保證單個變數操作的原子性,當涉及到多個變數的時候,CAS機制是無能為力的,而synchronized卻可以通過對整個代碼塊進行加鎖處理;再比如,版本號機制如果在查詢資料的時候是針對表1,而更新資料的時候是針對表2,也很難通過簡單的版本號來實作樂觀鎖,
- 競爭激烈程度,在競爭不激烈(出現并發沖突的概率比較小)的場景中,樂觀鎖更有優勢,因為悲觀鎖會鎖住代碼塊或資料,其他的執行緒無法同時訪問,必須等待上一個執行緒釋放鎖才能進入操作,會影響并發的回應速度,另外,加鎖和釋放鎖都需要消耗額外的系統資源,也會影響并發的處理速度,在競爭激烈(出現并發沖突的概率較大)的場景中,悲觀鎖則更有優勢,因為樂觀鎖在執行更新的時候,可能會因為資料被反復修改而更新失敗,進而不斷重試,造成CPU資源的浪費
總的來說樂觀鎖可以當做美圖秀秀,悲觀鎖可以當做PS,簡單場景樂觀鎖OK,大量場景下還是會存在問題
自旋鎖和非自旋鎖
自旋鎖(spinlock),是指嘗試獲取鎖的執行緒不會立即阻塞,而是采用回圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒背景關系切換的消耗,缺點是回圈會消耗CPU,其實和CAS的操作類似,也是樂觀鎖的一種實作形式,

自旋鎖不會使執行緒狀態發生切換,一直處于用戶態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的背景關系切換,執行速度快
非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要執行緒背景關系切換, (執行緒被阻塞后便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)
package com.nuih.lock;
import sun.misc.Unsafe;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 題目:實作一個自旋鎖
* 自旋鎖好處:回圈比較獲取直到成功為止,沒有類似wait的阻塞
*
* 通過cas操作完成自旋鎖,A執行緒先進來呼叫myLock方法自己持有鎖5秒鐘,
* B隨后進來后發現當前有執行緒持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖后B隨后搶到
*/
public class SpinLockDemo {
// 原子參考
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t come in ");
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName() + "\t invoked myUnLock");
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
},"B").start();
}
}
比較與適用場合
自旋和非自旋之間存在如下差異且因此在不同的場合使用:
- 自旋鎖不會進入內核態,避免了執行緒背景關系切換的開銷,而非自旋鎖會切換內核態
- 自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了執行緒切換的開銷,但它是要占用處理器時間的
如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被占用的時間很長,那么自旋的執行緒只會白白消耗處理器資源,而不會做任何有用的作業,反而會帶來性能上的浪費,
如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起執行緒了,自旋次數的默認值是10次
自適應自旋
在JDK 1.6中引入了自適應的自旋鎖,自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,
-
如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,并且持有鎖的執行緒正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長時間,比如100個回圈,
-
如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋程序,以避免浪費處理器資源
不過這也僅僅為區域優化,是一種基于預測的優化
公平鎖和非公平鎖
公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,非公平鎖是指多個執行緒獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的執行緒比先申請的執行緒優先獲取鎖,有可能,會造成優先級反轉或者饑餓現象,
- Java ReentrantLock而言,通過建構式指定該鎖是否是公平鎖,默認是非公平鎖,非公平鎖的優點在于吞吐量比公平鎖大,
- Synchronized也是一種非公平鎖,由于其并不像ReentrantLock是通過AQS的來實作執行緒調度,所以并沒有任何辦法使其變成公平鎖,
二者的實作方式有差異:
- 公平鎖的實作:并發環境中,每個執行緒在獲取鎖時會先查看此鎖維護的等待佇列,如果為空,或者當前執行緒是等待佇列的第一個,就占有鎖,否則就會加入到等待佇列中,以后按照FIFO的規則從佇列中取到自己
- 非公平鎖的實作:非公平鎖,直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖的那種方式
吞吐量大的情況下還是選擇非公平鎖
可重入鎖(遞回鎖)
可重入鎖(也叫做遞回鎖)指的是同一執行緒外層函式獲得鎖之后,內層遞回函式仍然能獲取該鎖的代碼,同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,也就是說,執行緒可以進入任何一個它已經擁有的鎖所同步著的代碼快,可重入鎖的最大作用是避免死鎖
package com.nuih.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyPhone implements Runnable {
public synchronized void sendMsg(){
System.out.println(Thread.currentThread().getName() + "\t invoked sendMsg");
sendEmail();
}
public synchronized void sendEmail(){
System.out.println(Thread.currentThread().getName() + "\t##### invoked sendEmail");
}
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t invoked get");
set();
}finally {
lock.unlock();
}
}
public void set(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t###### invoked set");
}finally {
lock.unlock();
}
}
}
/**
* 可重入鎖(也叫做遞回鎖)
*
* 指的是同一執行緒外層函式獲得鎖之后,內層遞回函式仍然能獲取該鎖的代碼,
* 在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,
*
* 也就是說,執行緒可以進入任何一個它已經擁有的鎖所同步著的代碼快,
*
* t1 invoked sendMsg() t1執行緒在外層方法獲取鎖的時候
* t1 ##### invoked sendEmail t1在進入內層方法會自動獲取鎖
*
* t2 invoked sendMsg()
* t2 ##### invoked sendEmail
*/
public class ReentrantLockDemo {
public static void main(String[] args) throws InterruptedException {
MyPhone myPhone = new MyPhone();
new Thread(() -> {
myPhone.sendMsg();
},"t1").start();
new Thread(() -> {
myPhone.sendMsg();
},"t2").start(); //Synchronized的可重入實作
TimeUnit.SECONDS.sleep(1);
System.out.println("\n\n\n\n");
Thread t3 = new Thread(myPhone,"t3"); //ReentrantLock的可重入實作
t3.start();
Thread t4 = new Thread(myPhone,"t4");
t4.start();
}
}
在上面代碼段中,執行 sendMsg方法需要獲得當前物件作為監視器的物件鎖,但方法中又呼叫了 sendEmail的同步方法,
- 如果鎖是具有可重入性的話,那么該執行緒在呼叫 sendEmail時并不需要再次獲得當前物件的鎖,可以直接進入 sendEmail方法進行操作,
- 如果鎖是不具有可重入性的話,那么該執行緒在呼叫 sendEmail前會等待當前物件鎖的釋放,實際上該物件鎖已被當前執行緒所持有,不可能再次獲得,
如果鎖是不具有可重入性特點的話,那么執行緒在呼叫同步方法、含有鎖的方法時就會產生死鎖,
獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個執行緒所持有,共享鎖是指該鎖可被多個執行緒所持有,對于Java ReentrantLock和Synchronized而言,其是獨享鎖,但是對于Lock的另一個實作類ReentrantReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖,讀鎖的共享鎖可保證并發讀是非常高效的,讀寫,寫讀 ,寫寫的程序是互斥的,獨享鎖與共享鎖也是通過AQS來實作的,通過實作不同的方法,來實作獨享或者共享,
上面講的獨享鎖/共享鎖是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實作,互斥鎖在Java中的具體實作就是ReentrantLock,讀寫鎖在Java中的具體實作就是ReentrantReadWriteLock
package com.nuih.lock;
import com.nuih.map.HashMap;
import com.nuih.map.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t 開始寫入:" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 開始讀取");
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + result);
}finally {
readWriteLock.readLock().unlock();
}
}
}
如果只進行讀取
/**
* 多個執行緒同時讀一個資源類沒有任何問題,所以為了滿足并發量,讀取共享資源應該可以同時進行
* 如果又一個執行緒想去寫共享資源了,就不應該再有其它執行緒可以對該資源進行讀或寫
* 小總結:
* 讀-讀可以共存
* 讀-寫不能共存
* 寫-寫不能共存
*
* 寫操作:原子+獨占,中間程序必須一個完整的統一體,中間不許被分割
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for(int i=0;i<5;i++){
int finalI = i;
new Thread(() -> {
myCache.get(finalI+"");
},"B"+String.valueOf(i)).start();
}
}
}
列印結果為,可以看到執行緒是交替執行的
B0 開始讀取
B4 開始讀取
B1 開始讀取
B3 開始讀取
B2 開始讀取
B2 讀取完成:null
B3 讀取完成:null
B1 讀取完成:null
B4 讀取完成:null
B0 讀取完成:null
如果只進行寫入:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for(int i=0;i<5;i++){
int finalI = i;
new Thread(() -> {
myCache.put(finalI+"",finalI);
},"A"+String.valueOf(i)).start();
}
}
}
列印結果為,可以看到執行緒是挨個執行的,
A1 開始寫入:1
A1 寫入完成
A0 開始寫入:0
A0 寫入完成
A2 開始寫入:2
A2 寫入完成
A3 開始寫入:3
A3 寫入完成
A4 開始寫入:4
A4 寫入完成
鎖的優化措施
鎖的狀態變化分為兩種,鎖的消除、鎖的粗化、記憶體級別的鎖升級以及分段鎖的實作,
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除,
鎖消除的主要判定依據來源于逃逸分析的資料支持,如果判斷在一段代碼中,堆上的所有資料都不會逃逸出去從而被其他執行緒訪問到,那就可以把它們當做堆疊上資料對待,認為它們是執行緒私有的,同步加鎖自然就無須進行,
public String concatString(String s1,String s2){
StringBuffer sb=new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
每個StringBuffer.append方法中都有一個同步塊,鎖就是sb物件,虛擬機觀察變數sb,很快就會發現它的動態作用域被限制在concatString方法內部,sb的所有參考永遠不會逃逸到concatString方法之外,其他執行緒無法訪問到它
代碼中concatString方法中的區域物件sb,就只在該方法內的作用域有效,不同執行緒同時呼叫concatString方法時,都會創建不同的sb物件,因此此時的append操作若是使用同步操作,就是白白浪費的系統資源因此,雖然這里有鎖,但是可以被安全地消除掉,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執行了,
鎖粗化
原則上,我們在撰寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小——只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量盡可能變小,如果存在鎖競爭,那等待鎖的執行緒也能盡快拿到鎖,
大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個物件反復加鎖和解鎖,甚至加鎖操作是出現在回圈體中的,那即使沒有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗,
for(int i=0;i<size;i++){
synchronized(lock){
}
如果虛擬機探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部
synchronized(lock){
for(int i=0;i<size;i++){
}
}
上述代碼中,擴展到for回圈之外加鎖,這樣只需要加鎖一次就可以了,
鎖升級
因為Synchronized太重了,所以在虛擬機層面上進行了優化,偏向鎖/輕量級鎖/重量級鎖這三種鎖是指鎖的狀態,Java 5通過引入鎖升級的機制來實作高效Synchronized,這三種鎖的狀態是通過物件監視器在物件頭中的欄位來表明的,
- 偏向鎖是指一段同步代碼一直被一個執行緒所訪問,那么該執行緒會自動獲取鎖,降低獲取鎖的代價,
- 輕量級鎖是指當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能,
- 重量級鎖是指當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖,重量級鎖會讓其他申請的執行緒進入阻塞,性能降低,
其實在BlogJava并發機制的底層實作詳細介紹過,這里不再贅述,這里給出簡單的狀態圖:

分段鎖
分段鎖分段鎖其實是一種鎖的設計,并不是具體的一種鎖,對于ConcurrentHashMap而言,其并發的實作就是通過分段鎖的形式來實作高效的并發操作,我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想
- ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap(JDK7與JDK8中HashMap的實作)的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock),
- 當需要put元素的時候,并不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然后對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實作了真正的并行的插入,
- 在統計size的時候,可就是獲取hashmap全域資訊的時候,就需要獲取所有的分段鎖才能統計,
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作,
無同步方案
無同步方案包含兩個內容,可重入的代碼塊和執行緒本地存盤方案Thread Local Storage
可重入代碼(Reentrant Code)
這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞回呼叫它本身),而在控制權回傳后,原來的程式不會出現任何錯誤,相對執行緒安全來說,可重入性是更基本的特性,它可以保證執行緒安全,即所有的可重入的代碼都是執行緒安全的,但是并非所有的執行緒安全的代碼都是可重入的,可重入代碼有一些共同的特征
- 不依賴存盤在堆上的資料和公用的系統資源
- 用到的狀態量都由引數中傳入
- 不呼叫非可重入的方法等
我們可以通過一個簡單的原則來判斷代碼是否具備可重入性:如果一個方法,它的回傳結果是可以預測的,只要輸入了相同的資料,就都能回傳相同的結果,那它就滿足可重入性的要求,當然也就是執行緒安全的,
執行緒本地存盤(Thread Local Storage)
ThreadLocal提供了執行緒的區域變數,每個執行緒都可以通過set()和get()來對這個區域變數進行操作,但不會和其他執行緒的區域變數進行沖突,實作了執行緒的資料隔離,簡要言之:往ThreadLocal中填充的變數屬于當前執行緒,該變數對其他執行緒而言是隔離的,舉個例子
public class MyThreadLocal {
// 采用匿名內部類的方式來重寫initialValue方法
private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>() {
/**
* ThreadLocal沒有被當前執行緒賦值時或當前執行緒剛呼叫remove方法后呼叫get方法,回傳此方法值
*/
@Override
protected Object initialValue() {
System.out.println("呼叫get方法時,當前執行緒共享變數沒有設定,呼叫initialValue獲取默認值!");
return null;
}
};
// 操縱int型別的任務執行緒
public static class MyIntegerTask implements Runnable {
private String name;
MyIntegerTask(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
// ThreadLocal.get方法獲取執行緒變數
if (null == MyThreadLocal.threadLocal.get()) {
// ThreadLocal.et方法設定執行緒變數
MyThreadLocal.threadLocal.set(0);
System.out.println("執行緒" + name + ": 0");
} else {
int num = (Integer) MyThreadLocal.threadLocal.get();
MyThreadLocal.threadLocal.set(num + 1);
System.out.println("執行緒" + name + ": " + MyThreadLocal.threadLocal.get());
if (i == 3) {
MyThreadLocal.threadLocal.remove();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 操縱string型別的任務執行緒
public static class MyStringTask implements Runnable {
private String name;
MyStringTask(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
if (null == MyThreadLocal.threadLocal.get()) {
MyThreadLocal.threadLocal.set("a");
System.out.println("執行緒" + name + ": a");
} else {
String str = (String) MyThreadLocal.threadLocal.get();
MyThreadLocal.threadLocal.set(str + "a");
System.out.println("執行緒" + name + ": " + MyThreadLocal.threadLocal.get());
}
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new MyIntegerTask("IntegerTask1")).start();
new Thread(new MyStringTask("StringTask1")).start();
}
}
運行結果為:
呼叫get方法時,當前執行緒共享變數沒有設定,呼叫initialValue獲取默認值!
執行緒IntegerTask1: 0
呼叫get方法時,當前執行緒共享變數沒有設定,呼叫initialValue獲取默認值!
執行緒StringTask1: a
執行緒StringTask1: aa
執行緒IntegerTask1: 1
執行緒StringTask1: aaa
執行緒IntegerTask1: 2
執行緒StringTask1: aaaa
執行緒IntegerTask1: 3
執行緒StringTask1: aaaaa
呼叫get方法時,當前執行緒共享變數沒有設定,呼叫initialValue獲取默認值!
執行緒IntegerTask1: 0
對于多執行緒資源共享的問題,同步機制采用了以時間換空間的方式,而ThreadLocal采用了以空間換時間的方式,前者僅提供一份變數,讓不同的執行緒排隊訪問,而后者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響

可以通過java.lang.ThreadLocal類來實作執行緒本地存盤的功能,
- 每一個執行緒的Thread物件中都有一個
ThreadLocalMap物件, ThreadLocalMap物件存盤了一組以ThreadLocal.threadLocalHashCode為鍵,以本地執行緒變數為值的K-V值對,
ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,每一個ThreadLocal物件都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以在執行緒K-V值對中找回對應的本地執行緒變數,
總結
執行緒安全的實作方式共有三種,一種是互斥阻塞同步,一種是非阻塞同步,還有一種是無同步方案,整篇Blog詳細討論了這三種方式以及具體實作
阻塞同步
互斥同步最主要的問題就是進行執行緒阻塞和喚醒所帶來的性能問題,因此這種同步也稱為阻塞同步,從處理問題的方式上說,互斥同步屬于一種悲觀的并發策略,總是認為只要不去做正確的同步措施(例如加鎖),那就肯定會出現問題,無論共享資料是否真的會出現競爭,它都要進行加鎖(這里討論的是概念模型,實際上虛擬機會優化掉很大一部分不必要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的執行緒需要喚醒等操作,我們介紹了三種常用鎖的實作
- Synchronized,它是一個:非公平,悲觀,獨享,互斥,可重入的重量級鎖
- ReentrantLock,它是一個:默認非公平但可實作公平的,悲觀,獨享,互斥,可重入的重量級鎖,
- ReentrantReadWriteLocK,它是一個,默認非公平但可實作公平的,悲觀,寫獨享/讀共享,讀寫,可重入的重量級鎖
我們詳細比較了每一類鎖的分類
非阻塞同步
我們有了另外一個選擇:基于沖突檢測的樂觀并發策略,通俗地說,就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了沖突,那就再采取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功為止),這種樂觀的并發策略的許多實作都不需要把執行緒掛起,因此這種同步操作稱為非阻塞同步(Non-Blocking Synchronization),我們介紹了CAS自旋鎖以及自旋鎖的優化,自旋操作和版本號的方案
無同步方案
要保證執行緒安全,并不是一定就要進行同步,兩者沒有因果關系,同步只是保證共享資料爭用時的正確性的手段,如果一個方法本來就不涉及共享資料,那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是執行緒安全的,我們共介紹了無同步代碼和ThreadLocal兩種方案,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/265854.html
標籤:其他
