1. 并發(fā)競(jìng)爭(zhēng)概述
竟態(tài)條件:多線程在臨界區(qū)執(zhí)行,由于代碼執(zhí)行序列不可預(yù)知而導(dǎo)致無(wú)法預(yù)測(cè)結(jié)果
解決方式:
(1)阻塞式:sync, Lock(ReentrantLock)
(2)非阻塞式:Cas方式(自旋式)
2. Synchronized
2.1 使用方法
(1)實(shí)例方法加鎖:鎖住實(shí)例對(duì)象
public class MyClass {
private int count;
public synchronized void increment() {
count++;
}
}
在實(shí)例化對(duì)象上進(jìn)行鎖的判斷
(2)靜態(tài)方法加鎖:鎖住類(lèi)對(duì)象
public class MyClass {
private static int count;
public static synchronized void increment() {
count++;
}
}
在靜態(tài)類(lèi)MyClass上進(jìn)行鎖的判斷
(3)加鎖在代碼塊上:
public class MyClass {
private int count;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
我們創(chuàng)建了一個(gè)名為 lock 的對(duì)象,并在每個(gè)方法中使用 synchronized 關(guān)鍵字來(lái)獲取這個(gè)對(duì)象的鎖。當(dāng)一個(gè)線程進(jìn)入 synchronized 代碼塊時(shí),它會(huì)獲取 lock 對(duì)象的鎖,然后執(zhí)行代碼塊中的代碼。其他線程如果想要進(jìn)入這個(gè)代碼塊,必須等待當(dāng)前線程釋放鎖之后才能獲取鎖并執(zhí)行代碼塊中的代碼。
2.2 synchronized鎖競(jìng)爭(zhēng)

2.3 synchronized加鎖原理
(1)早期實(shí)現(xiàn):sync為JVM內(nèi)置鎖,(JVM層面)基于Monitor(監(jiān)視器)機(jī)制實(shí)現(xiàn),(Linux層面)依賴底層操作系統(tǒng)的Mutux(互斥量)實(shí)現(xiàn),由于進(jìn)行了內(nèi)核態(tài)及用戶態(tài)的切換,性能較低
(2)優(yōu)化后:通過(guò)鎖升級(jí)實(shí)現(xiàn)加鎖過(guò)程:偏向鎖,自旋鎖,輕量級(jí),重量級(jí)
(3)monitor(監(jiān)視器)原理:
同步方法由Au_SYNCHRONIZED標(biāo)志實(shí)現(xiàn), 同步代碼塊由monitorenter與monitorexit實(shí)現(xiàn);
是管理共享變量以及對(duì)共享變量操作的過(guò)程,讓它們支持并發(fā);
synchronized中wait(), notify(), notifyAll()等方法有monitor實(shí)現(xiàn);
補(bǔ)充:
java線程等待方法:sleep, wait, join, unpark
| 名稱(chēng) | 作用 | 特點(diǎn) |
|---|---|---|
| sleep | 讓當(dāng)前線程暫停執(zhí)行一段時(shí)間,再繼續(xù)執(zhí)行 | Thread 類(lèi)的靜態(tài)方法線程不會(huì)釋放鎖,其他線程無(wú)法訪問(wèn)被鎖定的資源 |
| wait | 讓當(dāng)前線程暫停執(zhí)行,直到其他線程調(diào)用該對(duì)象的 notify() 或 notifyAll() 方法喚醒它 | 在調(diào)用 wait() 方法時(shí),線程會(huì)釋放鎖,其他線程可以訪問(wèn)被鎖定的資源。wait() 方法必須在 synchronized 塊中調(diào)用,否則會(huì)拋出 IllegalMonitorStateException 異常 |
| join | 當(dāng)前線程會(huì)暫停執(zhí)行,直到另一個(gè)線程執(zhí)行完畢 | Thread 類(lèi)的實(shí)例方法,可以讓當(dāng)前線程等待另一個(gè)線程執(zhí)行完畢。在調(diào)用 join() 方法時(shí),當(dāng)前線程會(huì)暫停執(zhí)行,直到另一個(gè)線程執(zhí)行完畢。 |
| LockSupport.park() | LockSupport 類(lèi)的靜態(tài)方法,可以讓當(dāng)前線程暫停執(zhí)行,直到其他線程調(diào)用該線程的 unpark() 方法喚醒它 | LockSupport.park() 方法可以用于線程的阻塞和喚醒,例如等待某個(gè)條件的滿足后再繼續(xù)執(zhí)行 |
注:sleep() 方法和 wait() 方法可以在任何地方調(diào)用,而 join() 方法和 LockSupport.park() 方法必須在線程內(nèi)部調(diào)用。sleep() 方法和 wait() 方法可以讓線程等待一段時(shí)間后繼續(xù)執(zhí)行,而 join() 方法和 LockSupport.park() 方法可以讓線程等待其他線程的執(zhí)行或喚醒。
(4)sync加鎖解鎖流程圖:
(5)對(duì)象內(nèi)存布局

2.4 synchronized鎖升級(jí)
2.4.1 偏向鎖:
(1)流程:程序啟動(dòng)時(shí),將對(duì)象的標(biāo)記設(shè)置為偏向鎖,表示該對(duì)象目前沒(méi)有競(jìng)爭(zhēng),可以被當(dāng)前線程獨(dú)占;
當(dāng)其他線程訪問(wèn)該對(duì)象時(shí),會(huì)檢查該對(duì)象的標(biāo)記,如果是偏向鎖,則會(huì)判斷當(dāng)前線程是否是偏向鎖的擁有者,如果是,則直接獲取鎖,否則會(huì)升級(jí)為輕量級(jí)鎖或重量級(jí)鎖;
優(yōu)點(diǎn):減少鎖競(jìng)爭(zhēng)
(2)撤銷(xiāo)條件:
obj.wait() 偏向鎖撤銷(xiāo),升級(jí)為重量級(jí)鎖
notify(), notifyAll() 升級(jí)為輕量級(jí)鎖
2.4.2 鎖升級(jí)過(guò)程:
(1)在偏向鎖的基礎(chǔ)上,有線程P2加入競(jìng)爭(zhēng),會(huì)檢查該對(duì)象的標(biāo)記,如果當(dāng)前線程不是偏向鎖的擁有者,則會(huì)升級(jí)為輕量級(jí)鎖,輕量級(jí)鎖(又稱(chēng)自旋鎖)的功能是讓P2線程不斷自旋(while + cas)
注:若線程P1此時(shí)sleep,無(wú)競(jìng)爭(zhēng),則仍為偏向鎖
(2)線程自旋達(dá)到一定次數(shù)失敗,進(jìn)行線程阻塞(切換到內(nèi)核態(tài)),此時(shí)為重量級(jí)鎖;
重量級(jí)鎖是一種基于操作系統(tǒng)的鎖,它的作用是在獲取鎖時(shí),將當(dāng)前線程掛起,等待鎖的擁有者釋放鎖。重量級(jí)鎖的效率比較低,因?yàn)樗枰M(jìn)行線程的上下文切換和內(nèi)核態(tài)和用戶態(tài)之間的切換。
3. ReentrantLock原理
3.1 操作系統(tǒng)中的并發(fā)處理--管程

3.2 JVM層面對(duì)管程的實(shí)現(xiàn)
synchronized:
objectMonitor & cxq(cas owner)-> 等效于同步等待隊(duì)列
waitset -> 等效于條件等待隊(duì)列
3.3 java中管程實(shí)現(xiàn)
(1)抽象層面:
同步等待隊(duì)列實(shí)現(xiàn):CAS機(jī)制, volatile int state,等待隊(duì)列
條件等待隊(duì)列實(shí)現(xiàn):conditional await, signal(), signalAll(), 出隊(duì)入隊(duì)條件
(2)工具層面:
抽象隊(duì)列同步器AQS
3.4 AQS原理
java.util.concurrent包中年基礎(chǔ)能力組件,常用于實(shí)現(xiàn)依賴狀態(tài)同步器
3.4.1 特性
(1)阻塞等待隊(duì)列:由條件隊(duì)列,同步隊(duì)列共同實(shí)現(xiàn)
(2)共享/獨(dú)占鎖:共享鎖:semaphore(信號(hào)量), CountDownLatch
獨(dú)占鎖:ReentrantLock
(3)公平/非公平鎖
(4)是否可重入:state表示狀態(tài)
(5)是否允許中斷:設(shè)置中斷標(biāo)志位
3.4.2 兩種等待隊(duì)列
(1)同步等待隊(duì)列:維護(hù)獲取資源(鎖)失敗后的線程
(2)條件等待隊(duì)列:
調(diào)用await()時(shí)釋放鎖,線程加入條件隊(duì)列
signal()時(shí)喚醒條件隊(duì)列中的線程,加入同步等待隊(duì)列中,等待獲取

3.4.3 ReentrantLock應(yīng)用
解決庫(kù)存超賣(mài)問(wèn)題
public string reduceStock() {
int stock = Integer.parseInt(StringRedisTemplate.OPsForValue().get("stock"));
if (stock > 0) {
stock--;
StringRedisTemplate.opsForValue().set("stock", stock);
}
return "end";
}
在多線程下會(huì)造成stock超賣(mài)
3.4.4 小總結(jié)
解決并發(fā)問(wèn)題
(1)非阻塞式:cas + 自旋鎖
(2)阻塞式:synchronized, ReentrantLock
公平鎖/非公平鎖區(qū)別:
(1)非公平鎖效率更高(對(duì)CPU的使用效率高);不用經(jīng)歷從等待隊(duì)列喚醒線程的步驟,有新的任務(wù)嘗試獲取鎖資源即可成功
(2)公平鎖保證線程不會(huì)長(zhǎng)時(shí)間陷于饑餓狀態(tài)
3.4.5 條件隊(duì)列的使用場(chǎng)景
boolean volatile hashcig;
public static Conditional cond = lock.newCondition();
public static cigratte() {
lock.lock();
try {
while (!hashcig) {
try {
log("no cigurate, wait");
cond.await();
} catch(Exception e) e.print();
}
log("begin work);
finally {
lock.unlock();
}
}
}
喚醒邏輯
new Thread(() -> {
lock.lock();
try {
hashcig = true;
cond.signal("上煙了");
} ...
}
3.4.6 流程圖
4. AQS框架擴(kuò)展
4.1 semaphore信號(hào)量
(1)作用:實(shí)現(xiàn)互斥鎖,通過(guò)同時(shí)只能有一個(gè)線程能獲取信息量;用于實(shí)現(xiàn)限流功能;
(2)工作原理:設(shè)置窗口值,當(dāng)未達(dá)到該窗口值時(shí),工作線程正常工作;當(dāng)窗口值為0時(shí),不支持新的線程執(zhí)行任務(wù),新的線程阻塞,進(jìn)入阻塞隊(duì)列;當(dāng)上一批線程釋放資源后,到等待隊(duì)列中喚醒等待線程,重新執(zhí)行以上流程;
注:即根據(jù)窗口值一個(gè)批次一個(gè)批次執(zhí)行多線程任務(wù),上個(gè)批次執(zhí)行時(shí)窗口值為0,則后面的線程阻塞;

(3)舉例
semphore windows = new semphore(3); // 設(shè)置窗口值
for (;;) {
new Thread(new Runnable() {
public void run() {
try {
windows.acquire(); // 占用窗口, windows - 1
Thread.sleep();
} finally {
window.release(); // 釋放
}
}
}
}
4.2 CountDownLatch
(1)作用:是一個(gè)同步協(xié)作類(lèi),允許一個(gè)或多個(gè)線程等待,直到其他線程完成操作集;使用給定的count初始化,await后阻塞直到當(dāng)前計(jì)數(shù)值;由于countDown方法調(diào)用達(dá)到0,count為0后所有等待線程均釋放;
注:將所有線程阻塞,直到countDown減到0值,調(diào)用unpark喚醒阻塞線程
(2)實(shí)現(xiàn)原理:每次countDownLatch都執(zhí)行release(1)減1,當(dāng)減到0時(shí)調(diào)用unpark喚醒阻塞線程;即:state != 0,線程阻塞;state = 0,線程繼續(xù)執(zhí)行;
(3)使用場(chǎng)景圖:

4.3 CyclicBarrier 循環(huán)屏障
4.3.1 使多個(gè)線程在cyclicBarrier上阻塞,直到滿足某條件放開(kāi)

4.3.2 使用實(shí)例:
CyclicBarrier cyc = new CyclicBarrier(3);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.current().getName());
cyc.await(); // 未達(dá)到三個(gè)線程,則進(jìn)行線程阻塞;總計(jì)達(dá)到三個(gè)線程時(shí),繼續(xù)向下執(zhí)行
System.out.println(Thread.current().getName());
Thread.sleep(5000);
System.out.println(Thread.current().getName());
} catch (...) { }
}
}
4.3.3 使用場(chǎng)景:
實(shí)現(xiàn)人滿發(fā)車(chē)場(chǎng)景,線程池用于線程復(fù)用
AutoicInteger con = new AutoicInteger();
ThreadPoolExecutor thread = new ThreadPoolExecutor(5, 5, 1000, Timeunit.Seconds, new ArrayBlockingQueue<>(100), (r)->new Thread(r, con.addAndGet(1)), new ThreadPoolExecutor.Abortpolicy());
CyclicBarrier cyc = new CyclicBarrier(5, () -> System.out.print("start"));
for (int i = 0; i < 10; i++) {
ThreadPoolExecutor.submit(new Runner(cyc));
}
4.3.4 實(shí)現(xiàn)流程:
(1)首先加獨(dú)占鎖, 鎖住int state
(2)進(jìn)入條件隊(duì)列,阻塞線程,并釋放鎖;由于finally塊中的釋放鎖lock.unlock();需重新獲取鎖
(3)由于僅有同步隊(duì)列邏輯實(shí)現(xiàn)中有喚醒線程,并重新獲取鎖的實(shí)現(xiàn),這里進(jìn)行對(duì)進(jìn)入同步隊(duì)列的復(fù)用
(4)喚醒同步隊(duì)列綁定的線程節(jié)點(diǎn)
4.3.5 兩種重要的隊(duì)列:
(1)同步等待隊(duì)列:主要用于維護(hù)獲取鎖失敗時(shí)入隊(duì)的線程
(2)條件等待隊(duì)列:調(diào)用await()方法時(shí)會(huì)釋放鎖,線程會(huì)加入條件隊(duì)列;
調(diào)用signal()喚醒時(shí)將條件隊(duì)列中線程節(jié)點(diǎn)移動(dòng)到同步隊(duì)列中,等待再次獲取鎖
4.4 ReentrantReadWriteLock
針對(duì)讀多寫(xiě)少場(chǎng)景,該工具特性為:讀讀可并發(fā);寫(xiě)寫(xiě)、寫(xiě)讀互斥;提高讀寫(xiě)場(chǎng)景并發(fā)量
4.4.1 總結(jié),實(shí)現(xiàn)一把鎖有哪些設(shè)計(jì)?
(1)cas + 自旋嘗試
(2)管程方式:synchronized
(3)AQS方式:Cas + 同步隊(duì)列
(4)實(shí)現(xiàn)tryAcquired()方法 -> ReentrantLock等
4.4.2 如何設(shè)計(jì)并實(shí)現(xiàn)一把讀寫(xiě)分離鎖?
答:采用高地位打標(biāo)設(shè)計(jì)
高16位不為0:有讀鎖,每多一位表示多一個(gè)線程持有讀鎖,最高為65535個(gè)
低16位不為0:有寫(xiě)鎖,沒(méi)多一位表示多一次重入