前言
線程并發系列文章:
Java 線程基礎
Java 線程狀態
Java “優雅”地中斷線程-實踐篇
Java “優雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應用與原理
Java 并發"鎖"的本質(一步步實現鎖)
Java Synchronized實現互斥之應用與源碼初探
Java 對象頭分析與使用(Synchronized相關)
Java Synchronized 偏向鎖/輕量級鎖/重量級鎖的演變過程
Java Synchronized 重量級鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級鎖原理深入剖析下(同步篇)
Java并發之 AQS 深入解析(上)
Java并發之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發之 ReentrantLock 深入分析(與Synchronized區別)
Java 并發之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應用篇)
最詳細的圖文解析Java各種鎖(終極篇)
線程池必懂系列
前面的十幾篇文章都是從源碼的角度分析線程并發涉及到的知識點,本篇將重點總結、歸納、提煉知識點,盡量少貼代碼。遇到有疑惑的點,請查看對應文章的分析。
通過本篇文章,你將了解到:
1、鎖的全家福
2、如何驗證公平/非公平鎖
3、底層如何獲取鎖/釋放鎖
4、自旋鎖與自適應自旋
5、為什么需要等待/通知機制
1、鎖的全家福
2、如何驗證公平/非公平鎖
公平與非公平區別之處在于獲取鎖時的策略。
如上圖:
1、線程1持有鎖。
2、線程2、線程3、線程4 在同步隊列里排隊等候鎖。
這時線程5也想要獲取鎖,根據公平與否分為兩種不同策略。
公平鎖
線程5先判斷同步隊列是是否有線程在等待,明顯地此時同步隊列里有線程在等待,于是線程5加入到同步隊列的尾部等待。
非公平鎖
1、線程5不管同步隊列是否有線程等待,管他三七二十一先去搶鎖再說。若是運氣好就能直接撿到便宜獲取了鎖,若是失敗再去排隊。
2、線程5還是有機會撿便宜的,若是此時線程1剛好釋放了鎖,并喚醒線程2,線程2醒過來后去獲取鎖。若在線程2獲取鎖之前線程5就去搶鎖了,那么它會成功。它的成功對于線程2、線程3、線程4來說是不公平的。
我們知道ReentrantLock 可實現公平/非公平鎖,來驗證一下。
先來驗證公平鎖:
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() + " 啟動了,準備獲取鎖");
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 按順序獲取鎖,實際上拿到鎖也是按照這順序的。
因此,符合先到先得,是公平的。
再來驗證非公平鎖
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() + " 啟動了,準備獲取鎖");
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();
}
}
打印如下:
這倆張圖結合來看:
1、第一張圖:線程1~線程10 依次調用lock搶鎖,然后主線程開始搶鎖。
2、只要有一次能夠證明主線成比線程1~線程10之間的某個線程先獲得鎖,那么就證明該鎖為非公平鎖。
3、第二張圖:主線程比線程4~線程10 先獲得了鎖,說明過程是非公平的。
值得注意的是:
此處使用tryLock()搶占鎖,tryLock()和lock(非公平模式)核心邏輯是一樣的。
3、底層如何獲取鎖/釋放鎖
一直在提線程獲取了鎖,線程釋放了鎖,到底這個邏輯如何實現的呢?
從第一張全家福的圖,可以看出鎖的基本數據結構包含:
共享鎖變量、volatile、CAS、同步隊列。
假設設定共享變量為:volatile int threadId。
threadId == 0表示當前沒有線程獲取鎖,thread !=0 表示有線程占有了鎖。
獲取鎖
1、線程調用 CAS(threadId, 0, 1),預期threadId == 0, 若是符合預期,則將threadId設置為1,CAS成功說明成功獲取了鎖。
2、若是CAS失敗,說明threadId != 0,進而說明有已經有別的線程修改了threadId,因此線程獲取鎖失敗,然后加入到同步隊列。
釋放鎖
1、持有鎖的線程不需要鎖后要釋放鎖,假設是獨占鎖(互斥),因為同時只有一個線程能獲取鎖,因此釋放鎖時修改threadId不需要CAS,直接threadId == 0,說明釋放鎖成功。
2、成功后,喚醒在同步隊列里等待的線程。
synchronized 和 AQS 獲取/釋放鎖核心思想就是上面幾步,只是控制得更復雜,精細,考慮得更全面。
注:CAS(threadId, xx, xx)是偽代碼
4、自旋鎖與自適應自旋
很多文章說CAS是自旋鎖,這說法是有問題的,本質上沒有完全理解CAS功能和鎖。
1、CAS 全稱是比較與交換,若是內存值與期望值一致,說明沒有其它線程更改目標變量,因此可以放心地將目標變量修改為新值。
2、CAS 是原子操作,底層是CPU指令。
3、CAS 只是一次嘗試修改目標變量的操作,結果要么成功,要么失敗,最后調用都會返回。
通過上個小結的分析,我們知道synchronized、AQS底層獲取/釋放鎖都是依賴CAS的,難道說synchronized、AQS 也是自旋鎖,顯然不是。
自旋鎖是不會阻塞的,而CAS也不會阻塞,因此可以利用CAS實現自旋鎖:
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分別調用lock()上鎖。
1、線程1調用lock(),因為atomicInteger== 0,所以suc == true,線程1成功獲取鎖。
2、此時線程2也調用lock(),因為atomicInteger==1,說明鎖被占用了,所以suc==false,然而線程2并不阻塞,一直循環去修改。只要線程1不釋放鎖,那么線程2永遠獲取不了鎖。
以上就是自旋鎖的實現,可以看出:
1、自旋鎖最大限度避免了線程掛起/與喚醒,避免上下文切換,但是無限制的自旋也會徒勞占用CPU資源。
2、因此自選鎖適用于線程執行臨界區比較快的場景,也就是獲得鎖后,快速釋放了鎖。
既想要自旋,又要避免無限制自旋,因此引入了自適應自旋:
class MyLock {
AtomicInteger atomicInteger = new AtomicInteger(0);
//最大自旋次數
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++;
}
}
}
可以看出,給自旋設置了最大自旋次數,若還是沒能獲取到鎖,則退出死循環。
實際上synchronized、ReentrantReadWriteLock 等的實現里,同樣為了盡量避免線程掛起/喚醒,在搶占鎖的過程中也是采用了自旋(自適應自旋)的思想,但這只是它們鎖實現的以小部分,它們并不是自旋鎖。
5、為什么需要等待/通知機制
先看獨占鎖的偽代碼:
//Thread1
myLock.lock();
{
//臨界區代碼
}
myLock.unLock();
//Thread2
myLock.lock();
{
//臨界區代碼
}
myLock.unLock();
Thread1、Thread2 互斥拿到鎖后各干各的,互不干涉,相安無事。
若是現在Thread1、Thread2 需要配合做事,如:
//Thread1
myLock.lock();
{
//臨界區代碼
while (flag == false)
wait();
//繼續做事
}
myLock.unLock();
//Thread2
myLock.lock();
{
//臨界區代碼
flag = true;
notify();
//繼續做事
}
myLock.unLock();
如上代碼,Thread1需要判斷flag == true才會往下運行,而這個值需要Thread2來修改,Thread1、Thread2 兩者間有協作關系。于是Thread1需要調用wait 釋放鎖,并阻塞等待。Thread2在Thread1釋放鎖后拿到鎖,修改flag,然后notify 喚醒Thread1(喚醒時機在Thread2執行完臨界區代碼并釋放鎖后)。Thread1 被喚醒后繼續搶鎖,然后判斷flag==true,繼續做事。
于是,Thread1、Thread2愉快配合完成工作。
為啥wait/notify 需要先獲取鎖呢?flag 是線程間共享變量,需要在并發條件下正確訪問,因此需要鎖。
至此,線程并發系列文章暫時告一段落了。大家對這系列文章有疑惑,請評論留言。
本文基于jdk 1.8。