前言
線程并發(fā)系列文章:
Java 線程基礎(chǔ)
Java 線程狀態(tài)
Java “優(yōu)雅”地中斷線程-實(shí)踐篇
Java “優(yōu)雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應(yīng)用與原理
Java 并發(fā)"鎖"的本質(zhì)(一步步實(shí)現(xiàn)鎖)
Java Synchronized實(shí)現(xiàn)互斥之應(yīng)用與源碼初探
Java 對象頭分析與使用(Synchronized相關(guān))
Java Synchronized 偏向鎖/輕量級(jí)鎖/重量級(jí)鎖的演變過程
Java Synchronized 重量級(jí)鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級(jí)鎖原理深入剖析下(同步篇)
Java并發(fā)之 AQS 深入解析(上)
Java并發(fā)之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發(fā)之 ReentrantLock 深入分析(與Synchronized區(qū)別)
Java 并發(fā)之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應(yīng)用篇)
最詳細(xì)的圖文解析Java各種鎖(終極篇)
線程池必懂系列
前面的十幾篇文章都是從源碼的角度分析線程并發(fā)涉及到的知識(shí)點(diǎn)著隆,本篇將重點(diǎn)總結(jié)奖磁、歸納耸彪、提煉知識(shí)點(diǎn)陈瘦,盡量少貼代碼。遇到有疑惑的點(diǎn)躯舔,請查看對應(yīng)文章的分析。
通過本篇文章,你將了解到:
1髓迎、鎖的全家福
2、如何驗(yàn)證公平/非公平鎖
3建丧、底層如何獲取鎖/釋放鎖
4排龄、自旋鎖與自適應(yīng)自旋
5、為什么需要等待/通知機(jī)制
1翎朱、鎖的全家福
2橄维、如何驗(yàn)證公平/非公平鎖
公平與非公平區(qū)別之處在于獲取鎖時(shí)的策略。
如上圖:
1拴曲、線程1持有鎖争舞。
2、線程2澈灼、線程3竞川、線程4 在同步隊(duì)列里排隊(duì)等候鎖。
這時(shí)線程5也想要獲取鎖叁熔,根據(jù)公平與否分為兩種不同策略委乌。
公平鎖
線程5先判斷同步隊(duì)列是是否有線程在等待,明顯地此時(shí)同步隊(duì)列里有線程在等待荣回,于是線程5加入到同步隊(duì)列的尾部等待遭贸。
非公平鎖
1、線程5不管同步隊(duì)列是否有線程等待心软,管他三七二十一先去搶鎖再說革砸。若是運(yùn)氣好就能直接撿到便宜獲取了鎖,若是失敗再去排隊(duì)糯累。
2算利、線程5還是有機(jī)會(huì)撿便宜的,若是此時(shí)線程1剛好釋放了鎖泳姐,并喚醒線程2效拭,線程2醒過來后去獲取鎖。若在線程2獲取鎖之前線程5就去搶鎖了,那么它會(huì)成功缎患。它的成功對于線程2慕的、線程3、線程4來說是不公平的挤渔。
我們知道ReentrantLock 可實(shí)現(xiàn)公平/非公平鎖肮街,來驗(yàn)證一下。
先來驗(yàn)證公平鎖:
public class TestThread {
private ReentrantLock reentrantLock = new ReentrantLock(true);
private void testLock() {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(runnable);
thread.setName("線程" + (i + 1));
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 啟動(dòng)了判导,準(zhǔn)備獲取鎖");
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
};
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testLock();
}
}
打印如下:
可以看出嫉父,線程2、3眼刃、4绕辖、5 按順序獲取鎖,實(shí)際上拿到鎖也是按照這順序的擂红。
因此仪际,符合先到先得,是公平的昵骤。
再來驗(yàn)證非公平鎖
public class TestThread {
private ReentrantLock reentrantLock = new ReentrantLock(false);
private void testLock() {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.setName("線程" + (i + 1));
thread.start();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void testUnfair() {
try {
Thread.sleep(500);
while (true) {
System.out.println("+++++++我搶...+++++++");
boolean isLock = reentrantLock.tryLock();
if (isLock) {
System.out.println("========我搶到鎖了!!!===========");
reentrantLock.unlock();
return;
}
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 啟動(dòng)了树碱,準(zhǔn)備獲取鎖");
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
};
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testLock();
testThread.testUnfair();
}
}
打印如下:
這倆張圖結(jié)合來看:
1、第一張圖:線程1~線程10 依次調(diào)用lock搶鎖变秦,然后主線程開始搶鎖成榜。
2、只要有一次能夠證明主線成比線程1~線程10之間的某個(gè)線程先獲得鎖伴栓,那么就證明該鎖為非公平鎖伦连。
3雨饺、第二張圖:主線程比線程4~線程10 先獲得了鎖钳垮,說明過程是非公平的。
值得注意的是:
此處使用tryLock()搶占鎖额港,tryLock()和lock(非公平模式)核心邏輯是一樣的饺窿。
3、底層如何獲取鎖/釋放鎖
一直在提線程獲取了鎖移斩,線程釋放了鎖肚医,到底這個(gè)邏輯如何實(shí)現(xiàn)的呢?
從第一張全家福的圖向瓷,可以看出鎖的基本數(shù)據(jù)結(jié)構(gòu)包含:
共享鎖變量肠套、volatile、CAS猖任、同步隊(duì)列你稚。
假設(shè)設(shè)定共享變量為:volatile int threadId。
threadId == 0表示當(dāng)前沒有線程獲取鎖,thread !=0 表示有線程占有了鎖刁赖。
獲取鎖
1搁痛、線程調(diào)用 CAS(threadId, 0, 1),預(yù)期threadId == 0, 若是符合預(yù)期宇弛,則將threadId設(shè)置為1鸡典,CAS成功說明成功獲取了鎖。
2枪芒、若是CAS失敗彻况,說明threadId != 0,進(jìn)而說明有已經(jīng)有別的線程修改了threadId病苗,因此線程獲取鎖失敗疗垛,然后加入到同步隊(duì)列。
釋放鎖
1硫朦、持有鎖的線程不需要鎖后要釋放鎖贷腕,假設(shè)是獨(dú)占鎖(互斥),因?yàn)橥瑫r(shí)只有一個(gè)線程能獲取鎖咬展,因此釋放鎖時(shí)修改threadId不需要CAS泽裳,直接threadId == 0,說明釋放鎖成功破婆。
2涮总、成功后,喚醒在同步隊(duì)列里等待的線程祷舀。
synchronized 和 AQS 獲取/釋放鎖核心思想就是上面幾步瀑梗,只是控制得更復(fù)雜,精細(xì)裳扯,考慮得更全面抛丽。
注:CAS(threadId, xx, xx)是偽代碼
4、自旋鎖與自適應(yīng)自旋
很多文章說CAS是自旋鎖饰豺,這說法是有問題的亿鲜,本質(zhì)上沒有完全理解CAS功能和鎖。
1冤吨、CAS 全稱是比較與交換蒿柳,若是內(nèi)存值與期望值一致,說明沒有其它線程更改目標(biāo)變量漩蟆,因此可以放心地將目標(biāo)變量修改為新值垒探。
2、CAS 是原子操作怠李,底層是CPU指令圾叼。
3仔引、CAS 只是一次嘗試修改目標(biāo)變量的操作,結(jié)果要么成功褐奥,要么失敗咖耘,最后調(diào)用都會(huì)返回。
通過上個(gè)小結(jié)的分析撬码,我們知道synchronized儿倒、AQS底層獲取/釋放鎖都是依賴CAS的,難道說synchronized呜笑、AQS 也是自旋鎖夫否,顯然不是。
自旋鎖是不會(huì)阻塞的叫胁,而CAS也不會(huì)阻塞凰慈,因此可以利用CAS實(shí)現(xiàn)自旋鎖:
class MyLock {
AtomicInteger atomicInteger = new AtomicInteger(0);
private void lock() {
boolean suc = false;
do {
//底層是CAS
suc = atomicInteger.compareAndSet(0, 1);
} while (!suc);
}
}
如上所示,自定義鎖MyLock驼鹅,線程1微谓,線程2分別調(diào)用lock()上鎖。
1输钩、線程1調(diào)用lock()豺型,因?yàn)閍tomicInteger== 0,所以suc == true买乃,線程1成功獲取鎖姻氨。
2、此時(shí)線程2也調(diào)用lock()剪验,因?yàn)閍tomicInteger==1肴焊,說明鎖被占用了,所以suc==false功戚,然而線程2并不阻塞娶眷,一直循環(huán)去修改。只要線程1不釋放鎖疫铜,那么線程2永遠(yuǎn)獲取不了鎖茂浮。
以上就是自旋鎖的實(shí)現(xiàn)双谆,可以看出:
1壳咕、自旋鎖最大限度避免了線程掛起/與喚醒,避免上下文切換顽馋,但是無限制的自旋也會(huì)徒勞占用CPU資源谓厘。
2、因此自選鎖適用于線程執(zhí)行臨界區(qū)比較快的場景寸谜,也就是獲得鎖后竟稳,快速釋放了鎖。
既想要自旋,又要避免無限制自旋他爸,因此引入了自適應(yīng)自旋:
class MyLock {
AtomicInteger atomicInteger = new AtomicInteger(0);
//最大自旋次數(shù)
final int MAX_COUNT = 10;
int count = 0;
private void lock() {
boolean suc = false;
while (!suc && count <= MAX_COUNT) {
//底層是CAS
suc = atomicInteger.compareAndSet(0, 1);
if (!suc)
Thread.yield();
count++;
}
}
}
可以看出聂宾,給自旋設(shè)置了最大自旋次數(shù),若還是沒能獲取到鎖诊笤,則退出死循環(huán)系谐。
實(shí)際上synchronized、ReentrantReadWriteLock 等的實(shí)現(xiàn)里讨跟,同樣為了盡量避免線程掛起/喚醒纪他,在搶占鎖的過程中也是采用了自旋(自適應(yīng)自旋)的思想,但這只是它們鎖實(shí)現(xiàn)的以小部分晾匠,它們并不是自旋鎖茶袒。
5、為什么需要等待/通知機(jī)制
先看獨(dú)占鎖的偽代碼:
//Thread1
myLock.lock();
{
//臨界區(qū)代碼
}
myLock.unLock();
//Thread2
myLock.lock();
{
//臨界區(qū)代碼
}
myLock.unLock();
Thread1凉馆、Thread2 互斥拿到鎖后各干各的薪寓,互不干涉,相安無事澜共。
若是現(xiàn)在Thread1预愤、Thread2 需要配合做事,如:
//Thread1
myLock.lock();
{
//臨界區(qū)代碼
while (flag == false)
wait();
//繼續(xù)做事
}
myLock.unLock();
//Thread2
myLock.lock();
{
//臨界區(qū)代碼
flag = true;
notify();
//繼續(xù)做事
}
myLock.unLock();
如上代碼咳胃,Thread1需要判斷flag == true才會(huì)往下運(yùn)行植康,而這個(gè)值需要Thread2來修改,Thread1展懈、Thread2 兩者間有協(xié)作關(guān)系销睁。于是Thread1需要調(diào)用wait 釋放鎖,并阻塞等待存崖。Thread2在Thread1釋放鎖后拿到鎖冻记,修改flag,然后notify 喚醒Thread1(喚醒時(shí)機(jī)在Thread2執(zhí)行完臨界區(qū)代碼并釋放鎖后)来惧。Thread1 被喚醒后繼續(xù)搶鎖冗栗,然后判斷flag==true,繼續(xù)做事供搀。
于是隅居,Thread1、Thread2愉快配合完成工作葛虐。
為啥wait/notify 需要先獲取鎖呢胎源?flag 是線程間共享變量,需要在并發(fā)條件下正確訪問屿脐,因此需要鎖涕蚤。
至此宪卿,線程并發(fā)系列文章暫時(shí)告一段落了。大家對這系列文章有疑惑万栅,請?jiān)u論留言佑钾。
本文基于jdk 1.8。