4 千變?nèi)f化的鎖
4.1 Lock 接口
鎖是一種工具,用于控制對共享資源的訪問占调,我們已經(jīng)有了 synchronized 鎖入挣,為什么還需要 Lock 鎖呢操刀?
synchronized 鎖存在以下問題:
- 效率低:試圖獲取鎖時不能設(shè)定超時匕得,會一直等待
- 不夠靈活:加鎖釋放鎖時機單一
- 無法知道是否已經(jīng)獲取了鎖
4.2 Lock 常用 5 個方法
在Lock 中聲明了 4 個方法來獲取鎖:
-
lock()
獲取鎖,如果鎖被其他線程獲取罐盔,則進行等待担平。Lock 不會像synchronized 自動釋放鎖,即使發(fā)生異常芒率,也能爭取釋放鎖囤耳,Lock 需要在 finally 中釋放鎖,以保證在發(fā)生異常時鎖被正確釋放偶芍。lock()
方法不能被中斷充择,獲取不到鎖則會一直等待,一旦陷入死鎖lock()
會進入永久等待匪蟀。 -
unlock()
釋放鎖椎麦,如果當(dāng)前線程沒有持有該鎖調(diào)用該方法會拋出 IllegalMonitorStateException 異常
public class LockDemo {
public static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo = new LockDemo();
// 只有第一個線程能獲取鎖,另一個線程會一直等待
// new Thread(() -> lockDemo.testLock()).start();
// new Thread(() -> lockDemo.testLock()).start();
// 兩個線程都能獲取到鎖
new Thread(() -> lockDemo.testLock2()).start();
new Thread(() -> lockDemo.testLock2()).start();
}
// 發(fā)生異常沒有正確釋放鎖材彪,線程2會一直等待獲取鎖
public void testLock() {
lock.lock();
System.out.println("已經(jīng)獲取到了鎖");
// 模擬異常观挎,看能否正確釋放鎖
int a = 1 / 0;
lock.unlock();
}
// 在finally中釋放鎖,即使發(fā)生異常段化,也能正確釋放
public void testLock2() {
lock.lock();
try {
System.out.println("已經(jīng)獲取到了鎖");
// 模擬異常嘁捷,看能否正確釋放鎖
int a = 1 / 0;
} finally {
// 在finally中釋放鎖
lock.unlock();
}
}
}
-
tryLock()
嘗試獲取鎖,如果當(dāng)前鎖沒有被其他線程持有显熏,則獲取成功返回true雄嚣,否則返回false。該方法不會引起當(dāng)先線程阻塞喘蟆,非公平鎖缓升。 -
tryLock(long time)
嘗試獲取鎖夷磕,獲取成功返回true,如果超時時間到仍沒有獲取到鎖則返回false仔沿。相比lock()
更加強大坐桩,我們可以根據(jù)是否能夠獲取到鎖來決定后續(xù)行為。 -
lockInterruptibly()
嘗試獲取鎖封锉,等待鎖過程中允許被中斷绵跷,可以被thread.interrupt()
中斷,中斷后拋出InterruptedException成福。
4.3 Lock 的可見性
Monitor 鎖 Happen-Before 原則(synchronized和Lock):對一個鎖的解鎖碾局,對于后續(xù)其他線程同一個鎖的加鎖可見,這里的“后續(xù)”指的是時間上的先后順序奴艾,又叫管程鎖定原則净当。
Java編譯器會在生成指令序列時在適當(dāng)位置插入內(nèi)存屏障指令來禁止處理器重排序,來保證可見性蕴潦。
4.4 鎖的分類
鎖有多種分類方法像啼,根據(jù)不同的分類方法,相同的一個鎖可能屬于不同的類別潭苞,比如 ReentrantLock 既是互斥鎖忽冻,又是可重入鎖。
根據(jù)不同的分類標準此疹,鎖大致可以分為以下 6 種:
4.4.1 樂觀鎖和悲觀鎖
根據(jù)線程要不要鎖住同步資源僧诚,鎖可以分為樂觀鎖與悲觀鎖。樂觀鎖不鎖住資源蝗碎,樂觀的認為沒有人與自己競爭資源湖笨。
為什么誕生樂觀鎖?
悲觀鎖又稱互斥同步鎖蹦骑,具有以下缺點:
- 阻塞和喚醒帶來的性能劣勢慈省,用戶態(tài)核心態(tài)切換,檢查是否有阻塞線程需要被喚醒
- 可能永久阻塞脊串,如果持有鎖的線程被永久阻塞辫呻,比如死鎖等清钥,那么等待鎖的線程將永遠不會被執(zhí)行
- 優(yōu)先級反轉(zhuǎn)
悲觀鎖指對數(shù)據(jù)被外界修改持悲觀態(tài)度琼锋,認為數(shù)據(jù)很容易被其他線程修改,所以在數(shù)據(jù)處理前需要對數(shù)據(jù)進行加鎖祟昭。Java 中典型的悲觀鎖是 synchronized 和 Lock缕坎。
樂觀鎖認為修改數(shù)據(jù)在一般情況下不會造成沖突,所以在修改記錄前不會加鎖篡悟,但在數(shù)據(jù)提交更新時谜叹,才會對數(shù)據(jù)沖突與否進行檢測匾寝,檢查我修改數(shù)據(jù)期間,有沒有其他線程修改過荷腊,一般通過加 version 字段或 CAS 算法實現(xiàn)艳悔。Java中的典型樂觀鎖是原子類和并發(fā)容器。自旋鎖(CAS)是樂觀鎖的一種實現(xiàn)方式女仰。
在數(shù)據(jù)庫中就有對樂觀鎖的典型應(yīng)用:要更新一條數(shù)據(jù)猜年,首先查詢數(shù)據(jù)的version:select * from table;
然后更新語句,update set num=2疾忍,version=version+1 where version=1 and id=5,如果數(shù)據(jù)沒有被其他人修改乔外,則version與查詢數(shù)據(jù)時的version一直都為1,則可以修改成功一罩,并返回version=2杨幼,如果被其他人修改了,則重新查詢和更新數(shù)據(jù)聂渊。
開銷對比:
- 悲觀鎖的原始開銷要高于樂觀鎖差购,但是特點是一勞永逸。適合資源競爭激烈的情況汉嗽,持有鎖時間長的情況歹撒。
- 樂觀鎖如果自旋時間很長或不停重試,消耗的資源也會越來越多诊胞。適用于資源競爭不激烈的情況暖夭。
- 悲觀鎖要掛起和喚醒,悲觀的認為資源很難輪到自己使用撵孤,所以傻傻阻塞自己等待資源迈着,等別人用完了喚醒自己。樂觀鎖相信資源很快會輪到自己邪码,所以不停的詢問自己能不能用裕菠。
適用場景:
悲觀鎖,適合并發(fā)寫入多闭专,持有鎖時間較長的情況奴潘,如臨界區(qū)有IO操作,臨界區(qū)代碼復(fù)雜循環(huán)量大影钉,臨界區(qū)競爭激烈等情況
樂觀鎖画髓,適合并發(fā)寫入少,不加鎖能提高效率平委。如少寫多讀數(shù)據(jù)的場景奈虾,數(shù)據(jù)改變概率小,自旋次數(shù)少。
4.4.2 可重入鎖與非可重入鎖
根據(jù)同一個線程能否重復(fù)獲取同一把鎖肉微,鎖可以分為可重入鎖與非可重入鎖匾鸥。
為什么需要可重入鎖?
可重入鎖主要用在線程需要多次進入臨界區(qū)代碼時碉纳,需要使用可重入鎖勿负。具體的例子,比如一個synchronized方法需要調(diào)用另一個synchronized方法時劳曹“驶罚可以避免死鎖,也可以在同步方法被遞歸調(diào)用時使用厚者。
當(dāng)一個線程要獲取被其他線程持有的獨占鎖時躁劣,該線程會被阻塞,那么當(dāng)一個線程可以再次獲取它自己已經(jīng)持有的鎖库菲,即不被阻塞則稱為可重入鎖账忘,不可以再次獲取,即獲取時被阻塞熙宇,則稱為不可重入鎖鳖擒,。一定要注意是同一個線程烫止。Java 中典型的可重入鎖是 ReentrantLock 和 synchronized蒋荚。
synchronized 的可重入性質(zhì)見 Github示例,下面是對于 ReentrantLock 可重入性質(zhì)的演示馆蠕,main線程多次獲取已經(jīng)持有的lock鎖期升,getHoldCount() 表示:
public class ReentrantLockDemo {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
// 該線程可以多次獲取可重入鎖,并且不釋放鎖
new Thread(() -> demo.testReetrant()).start();
Thread.sleep(1000);
// 該線程嘗試獲取可重入鎖失敗互躬,因為鎖被上一個線程持有
new Thread(() -> demo.getLock()).start();
}
private void getLock() {
lock.lock();
System.out.println(Thread.currentThread().getName() + "獲取到了鎖");
}
/**
* 多次獲取可重入鎖
*/
private void testReetrant() {
// 輸出當(dāng)前線程持有鎖lock的次數(shù)
System.out.println(lock.getHoldCount());
// 當(dāng)前線程對lock加鎖
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
}
}
線程獲取可重入鎖有三種情況:
- 如果鎖已經(jīng)被其他線程持有播赁,則進入阻塞等待狀態(tài);
- 如果鎖沒有被其他線程持有吼渡,則獲得鎖容为,并將鎖的當(dāng)前持有者設(shè)置為當(dāng)前線程
- 如果鎖被當(dāng)前線程持有,則獲得鎖寺酪,并將鎖的重入計數(shù)器+1坎背,釋放鎖時會將計數(shù)器-1;
根據(jù)以上特點寄雀,可重入鎖的簡單實現(xiàn)如下:
public class Lock{
boolean isLocked = false; // 表示當(dāng)前鎖是否被線程持有
Thread lockedBy = null; // 表示當(dāng)前鎖被哪個線程持有
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
// 被鎖住且鎖的持有者不是當(dāng)前線程得滤,則進入阻塞等待狀態(tài)
this.wait();
}
isLocked = true;
// 可重入鎖,需要記錄當(dāng)前線程獲取該鎖的次數(shù)
lockedCount++;
// 標記該鎖被當(dāng)期線程持有
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
// 釋放鎖咙俩,重入計數(shù)器-1
lockedCount--;
if(lockedCount == 0){
isLocked = false;
// 釋放鎖耿戚,并喚醒其他等待獲取該鎖的線程
this.notify();
}
}
}
}
4.4.3 公平鎖與非公平鎖
根據(jù)多個線程獲取一把鎖時是否先到先得,可以分為公平鎖和不公平鎖阿趁。
公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間順序決定的膜蛔,也就是請求鎖早的線程更早獲取到鎖,即先來先得脖阵。而非公平鎖則在運行時插隊皂股,也就是先來不一定先得,但也不等于后來先得命黔。
Java 中 ReentrantLock 提供了公平鎖與非公平鎖的實現(xiàn):
- 公平鎖:
ReentrantLock fairLock = new ReetrantLock(true);
- 非公平鎖:
ReentrantLock unFairLock = new ReetrantLock(false);
不傳遞參數(shù)呜呐,默認非公平鎖。
為什么需要非公平鎖悍募?
為了提高效率蘑辑。假設(shè)線程 A 已經(jīng)持有了鎖,此時線程B請求該鎖則會阻塞被掛起Suspend坠宴。當(dāng)線程A釋放該鎖后洋魂,此時恰好有線程C也來請求此鎖,如果采取公平方式喜鼓,則線程B獲得該鎖副砍;如果采取非公平方式,則線程B和C都有可能獲取到鎖庄岖。
Java中這樣設(shè)計是為了提高效率豁翎,在上面的例子中,線程B被掛起隅忿,A釋放鎖后如果選擇去喚醒B心剥,則需要性能消耗和等待時間;如果直接給此時來請求鎖(未被掛起)的線程C背桐,則避免了喚醒操作的性能消耗刘陶,利用了這段空檔時間,性能更好牢撼〕赘簦總之,一定是能提升效率才會出現(xiàn)插隊情況熏版,否則一般不允許插隊纷责。
下圖中 Thread1 持有鎖時,等待鎖的隊列中有三個線程撼短,當(dāng)Thread1 釋放鎖時再膳,恰好 Thread5 請求了鎖,此時Thread5 就插隊到最前面獲取了鎖曲横。
在現(xiàn)實中也有類似的例子喂柒,比如排隊買早餐不瓶,攤主正在給A準備早餐,B則去旁邊座位等待了灾杰,A的早餐剛好做完時蚊丐,C來了,老板可能不會去花時間去叫并等待B艳吠,而會直接給C做麦备,提高自己的效率。
在沒有公平性需求的前提下盡量使用非公平鎖昭娩,因為公平鎖會帶來性能開銷凛篙。
公平鎖與非公平鎖的驗證見 Github示例
4.4.4 共享鎖與排它鎖
根據(jù)多個線程是否能夠共享同一把鎖,可以分為共享鎖與排它鎖栏渺。
共享鎖呛梆,又稱為讀鎖,可以同時被多個線程獲取磕诊。獲得共享鎖后可以查看但無法修改和刪除數(shù)據(jù)削彬,其他線程也可以同時獲得該共享鎖,也只能查看不能修改和刪除數(shù)據(jù)秀仲。ReentrantReadWriteLock 中的讀鎖就是共享鎖融痛,可以被多個線程同時獲取
排它鎖,又稱為獨占鎖神僵,不能被多個線程同時獲取雁刷,平時最常見的都是排它鎖,比如 synchronized保礼,ReentrantLock都是排它鎖沛励。
為什么需要共享鎖?
多個線程同時讀數(shù)據(jù)炮障,如果使用 ReentrantLock 則多個線程不能同時讀目派,降低了程序執(zhí)行效率。
如果在讀的地方使用共享鎖胁赢,寫的地方使用排它鎖企蹭。如果沒有寫鎖的情況下,讀是無阻塞的智末,提高了執(zhí)行效率谅摄。
讀寫鎖的規(guī)則
- 多個線程可以一起讀
- 一個線程寫的同時,其他線程不能讀也不能寫系馆。
- 一個線程讀的同時送漠,其他線程不能寫
Java中讀寫鎖的定義如下所示,完整的代碼示例見 Github
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void readText() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到讀鎖,正在讀取...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
readLock.unlock();
}
}
private static void writeText() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到寫鎖,正在寫入...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
writeLock.unlock();
}
}
讀寫鎖 ReentrantReadWriteLock 也可以通過構(gòu)造參數(shù)設(shè)置為公平鎖和非公平鎖由蘑,默認是非公平鎖闽寡。讀鎖插隊能和其他讀鎖共享代兵,可以提升效率,寫鎖是排它鎖爷狈,不會出現(xiàn)插隊情況植影,所以下面討論的均是讀鎖插隊的情況:
公平鎖是不允許插隊的,即先到先得淆院,不區(qū)分讀寫鎖何乎。但是讀寫鎖 ReentrantReadWriteLock 作為非公平鎖時插隊策略有以下兩種:
1. 誰能獲取誰優(yōu)先策略句惯,如下圖所示土辩,當(dāng) Thread2(R) 在獲得了讀鎖后,依次來了三個線程抢野,Thread3(W) 請求寫鎖拷淘,Thread4(R) 和 Thread5(R) 請求讀鎖。
誰能獲取誰優(yōu)先策略就是誰能獲取誰優(yōu)先指孤,Thread2(R) 持有讀鎖時启涯,Thread3(W)雖然來的早但是無法獲得寫鎖,Thread4(R) 和 Thread5(R) 來的晚但是可以獲取讀鎖恃轩,所以優(yōu)先出隊结洼。寫鎖因為是排它鎖,所以不存在插隊的情況叉跛。(注意這里與公平鎖章節(jié)所說的恰好釋放時請求鎖提高效率情況不同)
但是該策略存在一個缺點松忍,就是容易導(dǎo)致 Thread3(W) 出現(xiàn)饑餓問題,如果一直有其他線程來獲取讀鎖筷厘,那么 Thread3(W) 可能永遠請求不到寫鎖鸣峭,導(dǎo)致饑餓問題。從用戶角度出發(fā)酥艳,我先修改后讀取摊溶,但是修改晚于讀取生效也是不合理的。
2. 避免饑餓策略充石,如下圖所示莫换,當(dāng) Thread2(R) 在獲得了讀鎖后,依次來了兩個線程骤铃,Thread3(W) 請求寫鎖浓镜,Thread4(R) 請求讀鎖。
避免饑餓策略就是等待隊列頭元素是請求寫時劲厌,請求讀不能插隊膛薛,Thread2(R) 持有讀鎖時,Thread3(W)來的早但是無法獲得寫鎖补鼻,Thread4(R) 雖然可以獲取讀鎖哄啄,但是來的比寫鎖 Thread3(W) 晚雅任,所以也加入等待隊列。直至 Thread3(W) 獲取并釋放了寫鎖咨跌,Thread4(R) 才可以獲取讀鎖沪么。
避免饑餓策略雖然犧牲了一些效率,但是解決了饑餓問題锌半,并且更加符合人們的認知禽车,所以這也是 JDK 中讀寫鎖使用的插隊策略。
Java 中 ReentrantReadWriteLock 讀鎖插隊能提升效率刊殉,寫鎖是排它鎖殉摔,不會出現(xiàn)插隊情況,所以關(guān)于讀鎖插隊策略總結(jié)如下:
- 公平鎖:不允許插隊记焊,不區(qū)分讀寫鎖逸月,一律先到先得
- 非公平鎖:讀鎖僅在可以等待隊列頭部不是請求寫鎖的線程時可以插隊;如果等待隊列頭部是請求讀鎖遍膜,而當(dāng)目前持有寫鎖的線程恰好釋放寫鎖是碗硬,則新來的讀線程會插隊獲得讀鎖,這點與非公平鎖選擇接受新線程而不去喚醒等待線程出策略一致瓢颅。請求讀線程插隊代碼示例見 Github
// ReentrantReadWriteLock內(nèi)部類公平鎖源碼
// 公平鎖請求讀和請求寫都需要去排隊恩尾,除非隊列中沒有元素才去嘗試獲取鎖
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
// 判斷寫入線程是否應(yīng)該阻塞Block,如果隊列不為空挽懦,返回true表示應(yīng)該阻塞去排隊
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// 判斷讀取線程是否應(yīng)該阻塞Block翰意,如果隊列不為空,返回true表示應(yīng)該阻塞去排隊
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
// ReentrantReadWriteLock內(nèi)部類非公平鎖源碼
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 寫線程不阻塞掛起巾兆,直接嘗試插隊
final boolean writerShouldBlock() {
return false;
}
// 查看等待隊列第一個元素是不是排他鎖(寫鎖)
// 如果是返回true猎物,表示當(dāng)前請求讀線程應(yīng)該阻塞掛起
// 讀鎖不能插寫鎖的隊,
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
ReentrantReadWriteLock 鎖的升降級
為什么需要鎖的升降級角塑?
一個方法開始時需要寫入蔫磨,后面需要讀取,為了減小鎖的粒度圃伶,且方法執(zhí)行過程中不希望被打斷堤如。如果支持寫鎖降級為讀鎖,就可以減小寫鎖的粒度窒朋,在讀的部分搀罢,其他線程也可以一起來讀取,并且方法執(zhí)行不會被打斷侥猩。(釋放寫鎖重新獲取讀鎖可能會阻塞等待)
ReentrantReadWriteLock 鎖寫鎖可以降級為讀鎖提高執(zhí)行效率榔至,但不支持讀鎖升級為寫鎖,升級會將線程阻塞欺劳。ReentrantReadWriteLock 鎖升級降級示例代碼見Github
面試題:為什么ReentrantReadWriteLock不支持鎖的升級唧取?
線程A和B目前都持有讀鎖铅鲤,且都想升級為寫鎖淀弹,線程A會等待線程B釋放讀鎖后進行升級暮芭,線程B也會等待線程A釋放讀鎖后進行升級蜈彼,這樣就造成了死鎖嗤放。當(dāng)然,并不是說鎖是無法升級的粱侣,比如可以限制每次都只有一個線程可以進行鎖升級汛闸,這個需要具體的鎖去進行實現(xiàn)核无。
4.4.5 自旋鎖與阻塞鎖
根據(jù)等待鎖的方式可以分為自旋鎖和阻塞鎖韩容。
阻塞或喚醒一個 Java 線程需要操作同切換CPU狀態(tài)來完成款违,這個狀態(tài)
為什么需要自旋鎖?
java的線程是與操作系統(tǒng)原生線程一一對應(yīng)的宙攻,掛起和恢復(fù)線程都需要切換到內(nèi)核態(tài)中完成奠货。 當(dāng)一個線程獲取鎖失敗后介褥,會被切換到內(nèi)核態(tài)掛起座掘。當(dāng)該線程獲取到鎖時,又需要將其切換到內(nèi)核態(tài)來喚醒該線程柔滔,用戶態(tài)切換到核心態(tài)會消耗大量的系統(tǒng)資源溢陪。
自旋鎖則是線程獲取鎖時,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有睛廊,它不會馬上阻塞自己形真,而會進入忙循環(huán)(自旋)多次嘗試獲取(默認循環(huán)10次)超全,避免了線程狀態(tài)切換的開銷咆霜。
面試題:自旋鎖其實就是死循環(huán),死循環(huán)會導(dǎo)致 CPU 一個核心的使用率達到100%嘶朱,那為什么多個自旋鎖并沒有導(dǎo)致 CPU 使用率到達100%系統(tǒng)卡死呢蛾坯?
默認自旋最多10次,可以使用-XX:PreBlockSpin
(-XX:PreInFlateSpin
)修改該值疏遏,在JDK1.6 中引入了自適應(yīng)的自旋鎖脉课,自適應(yīng)意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定财异。因此倘零,自旋等待的時間必須有一定限度,如果自旋超過了限定的次數(shù)仍沒有成功獲取鎖戳寸,則會升級為重量級鎖呈驶,即使用傳統(tǒng)的方式去掛起線程了。
如果在同一個鎖對象上疫鹊,自旋等待剛剛成功獲取過鎖袖瞻,并且持有鎖的線程正在運行中跌穗,那么JVM會認為這次自旋也很有可能再次成功,進而將允許自旋等待更長時間虏辫,如果100次循環(huán)蚌吸。
如果對于某個鎖,自旋很少成功獲得過砌庄,那么以后獲取這個鎖時將可能省略掉自旋過程羹唠,避免浪費處理器資源。
以上參考《深入理解JVM p298》
4.4.6 可中斷鎖與不可中斷鎖
根據(jù)在等待鎖的過程中是否可以中斷分為可中斷鎖與不可中斷鎖娄昆。
在Java中佩微,synchronized 就是不可中斷鎖,一旦線程開始請求synchronized鎖萌焰,就會一直阻塞等待哺眯,直至獲得鎖。Lock 就是可中斷鎖扒俯,tryLock(time) 和 lockInterruptibly() 都能在請求鎖的過程中響應(yīng)中斷奶卓,實現(xiàn)原理就是檢測線程的中斷標志位,如果收到中斷信號則拋出異常撼玄。
ReentrantLock#tryLock(time) 使用AQS獲取鎖夺姑,每次AQS循環(huán)都會檢測中斷標志位,若標志位被修改掌猛,則拋出異常盏浙,中斷獲取鎖,源碼如下所示:
// ReentrantLock源碼
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
// 嘗試獲取鎖
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 判斷中斷標志位是否被修改荔茬,若被修改則拋出異常中斷獲取鎖
if (Thread.interrupted())
throw new InterruptedException();
// doAcquireNanos中使用AQS嘗試獲取鎖废膘,每次循環(huán)也都會檢測中斷標志位
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
4.5 鎖優(yōu)化
JDK1.6 實現(xiàn)了各種鎖優(yōu)化技術(shù),如自適應(yīng)自旋(Adaptive Spinning)慕蔚、鎖消除(Lock Elimination)丐黄、鎖粗化(Lock Coaresening)、輕量級鎖(LightWeight Locking)和偏向鎖(Biased Locking)等技術(shù)坊萝。這些技術(shù)都是為了線程之間更高效的共享數(shù)據(jù)孵稽,以及解決競爭問題。
4.5.1 自適應(yīng)自旋鎖
互斥同步中對性能最大的影響是阻塞的實現(xiàn)十偶,掛起線程和恢復(fù)線程的操作都需要操作系統(tǒng)切換到內(nèi)核態(tài)來完成菩鲜,這些操作對并發(fā)性能帶來很大壓力,
4.5.2 鎖消除
鎖消除指的是在保證線程安全的前提下惦积,JVM 刪除一些不必要的鎖接校。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享資源的競爭的鎖進行消除蛛勉。鎖消除主要判定依據(jù)是來源于逃逸分析的數(shù)據(jù)支持鹿寻,如果判斷一段代碼中,堆上的所有數(shù)據(jù)都不會逃逸出去而被其他線程訪問到诽凌,那就可以把他們當(dāng)做棧上數(shù)據(jù)來對待毡熏,認為是線程私有的,同步加鎖自然無需進行侣诵。(逃逸分析見深入理解JVM)
比如下面拼接字符串的代碼中痢法,JDK5之前Javac編譯器會將"+"拼接優(yōu)化為StringBuffer,JDK5之后會優(yōu)化為StringBuilder杜顺。
public String concatString0(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
查看 StringBuffer#append源碼如下所示财搁,使用了 synchronized 來保證拼接操作的線程安全。但是在上面的例子中躬络,變量 sb 作用于被限制在 concatString() 方法內(nèi)部尖奔,即變量 sb 是線程私有的,不會出現(xiàn)線程安全問題(如果sb定義到屬性中則會出現(xiàn)線程安全問題)穷当。雖然這里有鎖提茁,經(jīng)過JIT編譯之后,這段代碼會忽略掉 synchronized 鎖來執(zhí)行膘滨。
// StringBufferr#append源碼
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
4.5.3 鎖粗化
鎖消除指的是在保證線程安全的前提下甘凭,JVM 擴大鎖的同步范圍稀拐,來避免對同一對象的反復(fù)加鎖解鎖火邓。
原則上,編寫代碼時推薦將同步塊的范圍盡量縮小德撬,這樣能夠保證線程安全的同時讓等待線程盡快拿到鎖并發(fā)執(zhí)行铲咨。但是如果當(dāng)前線程一系列操作都是對同一個對象的反復(fù)加鎖和解鎖,甚至加鎖解鎖操作出現(xiàn)在循環(huán)體中蜓洪,那即使沒有線程競爭纤勒,頻繁加鎖解鎖也會導(dǎo)致不必要的性能損耗。
比如下面的拼接字符串操作隆檀,每次執(zhí)行 StringBuffer#append 方法都會對同一個對象 sb 加鎖摇天,下面代碼共執(zhí)行了 3 次加鎖解鎖操作。如果 JVM 檢測到這樣一串操作都對同一個對象反復(fù)加鎖解鎖恐仑,則會把加鎖同步的范圍擴展(粗化)到整個操作的外部泉坐,就會將下面代碼的加鎖同步范圍擴展到第一個 append() 之前和最后一個 append() 之后。
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
4.5.4 重量級鎖
在并發(fā)編程中 synchronized 一直是元老級角色裳仆,很多人都會稱呼synchronized 為重量級鎖腕让。在Java中每一個對象都可以作為鎖,synchronized 實現(xiàn)同步分為以下 3 種情況:
- 對于普通同步方法歧斟,鎖是當(dāng)前實例對象
- 對于靜態(tài)同步方法纯丸,鎖是當(dāng)前類的 Class 對象
- 對于同步方法快偏形,鎖是 synchronized 括號中設(shè)置的對象
JVM 會將 synchronized 翻譯成兩條指令:
monitorenter
和 monitorexit
,分別表示獲取鎖與釋放鎖觉鼻,這兩個命令總是成對出現(xiàn)俊扭。
但是在JDK6中為了對 synchronized 進行優(yōu)化,引入了輕量級鎖和偏向鎖坠陈,減少獲取鎖和釋放鎖帶來的性能消耗统扳。
4.5.4 輕量級鎖
輕量級鎖是在沒有多線程競爭的前提下,減少傳統(tǒng)重量級鎖使用操作系統(tǒng)互斥量Mutex產(chǎn)生的性能消耗畅姊。輕量級鎖的加鎖解鎖是通過CAS完成的咒钟,避免了使用互斥量的開銷,但是如果存在鎖競爭若未,輕量級鎖除了互斥量的操作還發(fā)生了CAS操作朱嘴,性能反而更慢〈趾希總之萍嬉,輕量級鎖是使用CAS方式修改鎖對象頭
Java中每一個對象都可以作為鎖,對象頭主要分為兩部分隙疚,MarkWord 和 指向方法區(qū)對象類型數(shù)據(jù)的指針壤追,如果是數(shù)組對象,還會存儲數(shù)組長度供屉。MarkWord 中存儲對象的哈希碼行冰,對象分代年齡,鎖狀態(tài)伶丐。
重量級鎖與輕量級鎖
重量級鎖加鎖方式:monitorenter -> 執(zhí)行同步代碼塊 -》 monitorexit
輕量級鎖加鎖方式:
CAS(設(shè)置對象頭輕量級鎖標志)-》執(zhí)行代碼塊-》CAS(重置對象頭輕量級鎖標志)
CAS相對于monitorenter和monitorexit的代價更小,如果輕量級鎖自旋超過一定次數(shù)哗魂,為了避免自旋浪費CPU性能肛走,則升級為重量級鎖。
自旋鎖與輕量級鎖
自旋鎖是為了減少線程掛起次數(shù)录别;
而輕量級鎖是在加鎖的時候,如何使用一種更高效的方式來加鎖组题。是 synchronized 的一種形態(tài)葫男,使用CAS也就是自旋鎖來實現(xiàn)。
鎖升級
synchronized 鎖一共有4種狀態(tài)往踢,鎖升級過程依次是:無鎖狀態(tài)腾誉、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)。這幾種狀態(tài)會隨著競爭逐漸升級利职,鎖可以升級但不能降級趣效。
面試題:為什么 synchronized 鎖能升級不能降級?
4.5.5 偏向鎖
偏向鎖會偏向第一個獲取它的線程猪贪,如果接下來執(zhí)行過程中跷敬,該鎖沒有被其他線程獲取,則持有偏向鎖的線程永遠不需要再同步热押。如果說輕量級鎖是在無競爭情況下用CAS避免同步中使用互斥量西傀,而偏向鎖是等到競爭出現(xiàn)才釋放鎖,減少釋放鎖帶來的性能消耗桶癣。
HotSpot 作者經(jīng)過研究發(fā)現(xiàn)拥褂,大多數(shù)情況下,鎖不僅不存在多線程競爭牙寞,而且總是由同一線程多次獲得饺鹃,為了讓線程獲得鎖的代價更低引入了偏向鎖。
當(dāng)一個線程訪問同步塊并獲取鎖時间雀,會在鎖對象頭存儲鎖偏向的線程ID悔详,以后該線程進入和退出同步塊都不需要進行CAS操作來加鎖和解鎖,只需要檢查對象頭中存儲的線程ID是否為當(dāng)前線程惹挟。
如果偏向線程ID等于當(dāng)前線程茄螃,表示線程已經(jīng)獲取了鎖。
如果偏向線程ID不等于當(dāng)前線程连锯,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程归苍。
如果不是偏向鎖,則使用CAS競爭鎖萎庭。
偏向鎖是等到競爭出現(xiàn)才釋放鎖霜医,減少釋放鎖帶來的性能消耗,所以當(dāng)其他線程嘗試競爭偏向鎖時驳规,持有偏向鎖的線程才會釋放鎖。
并沒有完全搞懂署海,更多參考《Java并發(fā)編程的藝術(shù) p13》和在《深入理解JVM p402》
4.6 ReentrantLock
學(xué)完AQS再來補充
4.7 ReentrantReadWriteLock
推薦閱讀
Java并發(fā)編程之美 - 翟陸續(xù) 內(nèi)容和慕課網(wǎng)玩轉(zhuǎn)Java并發(fā)類似吗购,可以配合閱讀,有豐富的源碼分析砸狞,實踐部分有10個小案例
Java并發(fā)編程實戰(zhàn) - 極客時間 內(nèi)容有深度捻勉,并發(fā)設(shè)計模式,分析了 4 個并發(fā)應(yīng)用案例 Guava RateLimiter刀森,Netty踱启,Disrupter 和 HiKariCP,還介紹了 4 種其他類型的并發(fā)模型 Actor,協(xié)程埠偿,CSP等
精通Java并發(fā)編程 - 哈維爾 非常多的案例透罢,幾乎每個知識點和章節(jié)都有案例,學(xué)習(xí)后能更熟悉Java并發(fā)的應(yīng)用
傳智播客8天并發(fā) 筆記有并發(fā)案例冠蒋,CPU原理等筆記羽圃,非常深入,后面畫時間學(xué)習(xí)一下精
https://www.cnblogs.com/nmwyqw/p/12787680.html
參考文檔
實戰(zhàn)Java高并發(fā)程序設(shè)計 - 葛一鳴