多線程核心要點(diǎn),你知道嗎?
多線程
線程的狀態(tài)。
一、線程池
- 提交任務(wù)時(shí) 4 種情況:
- 小于 corePoolSize addWorker()。
- 大于 corePoolSize workQueue.offer(command) 直接增加 task 如果增加失敗就拒絕。
- 拒絕策略
- AbortPolicy 拋出異常,默認(rèn)。
- CallerRunsPolicy 不使用線程池執(zhí)行。
- DiscardPolicy 直接丟棄。
- DiscardOldestPolicy 丟棄隊(duì)列中最舊的任務(wù)。
二、鎖
Sychronized 原理
用法:
- 方法
- 代碼塊
在 JDK 1.6 之前,synchronized 只有傳統(tǒng)的鎖機(jī)制,因此給開發(fā)者留下了 synchronized 關(guān)鍵字相比于其他同步機(jī)制性能不好的印象。在 JDK 1.6 引入了兩種新型鎖機(jī)制:偏向鎖和輕量級(jí)鎖,它們的引入是為了解決在沒有多線程競(jìng)爭(zhēng)或基本沒有競(jìng)爭(zhēng)的場(chǎng)景下因使用傳統(tǒng)鎖機(jī)制帶來(lái)的性能開銷問(wèn)題。
鎖的升級(jí): 偏向鎖->輕量級(jí)鎖->重量鎖
鎖的映射關(guān)系存在對(duì)象頭中的。32 位系統(tǒng)上各狀態(tài)如圖所示:
偏向鎖:
當(dāng) JVM 啟用了偏向鎖,那么新創(chuàng)建的對(duì)象都是可偏向狀態(tài),此時(shí) mark word 里的 thread id 為 0,表示未偏向任何線程
加鎖過(guò)程:
- 當(dāng)對(duì)象第一次被線程獲取鎖時(shí),發(fā)現(xiàn)是未偏向的,那就將 thread id 改為當(dāng)前線程 id,成功繼續(xù)執(zhí)行同步塊中的代碼,失敗則升級(jí)為輕量級(jí)鎖
- 當(dāng)被偏向的線程再次進(jìn)入同步塊時(shí),發(fā)現(xiàn)鎖偏向的就是當(dāng)前線程,通過(guò)一些額外檢查后就繼續(xù)執(zhí)行。
- 當(dāng)其他線程進(jìn)入同步塊,發(fā)現(xiàn)有偏向的線程了,會(huì)進(jìn)入撤銷偏向鎖邏輯。
解鎖過(guò)程:
- 棧中的最近一條 lock record 的 obj 字段設(shè)置為 null
輕量級(jí)鎖:
線程在執(zhí)行同步塊之前,JVM 會(huì)在線程的棧幀上建立一個(gè) Lock Record。其包括了一個(gè)存儲(chǔ)對(duì)象頭中的 mark word 的 Displaced Mark Word 以及一個(gè)對(duì)象頭指針。
加鎖過(guò)程:
- 在線程棧中創(chuàng)建一個(gè) Lock Record,將其 obj refercence 字段指向鎖對(duì)象。
- 通過(guò) CAS 指令將 Lock Record 地址放在對(duì)象頭的 mark word 中,如果對(duì)象是無(wú)鎖狀態(tài)則修改成功,代表獲取到了輕量級(jí)鎖。如果失敗進(jìn)入步驟 3
- 如果線程以及持有該鎖了,代表這是鎖重入,設(shè)置 Lock Record 第一部分(Displaced Mark Word)為 null,起到了一個(gè)重入計(jì)數(shù)器的作用。然后結(jié)束
- 走到這一步說(shuō)明發(fā)生了競(jìng)爭(zhēng),膨脹為重量鎖。
解鎖過(guò)程:
- 遍歷線程棧,找到所有 obj 字段等于當(dāng)前鎖對(duì)象的 Lock Record
- 如果 Lock Record 的 Displaced Mark Word 為 null,代表是一次重入,將 obj 設(shè)為 null 后 continue
- 如果 Lock Record 的 Displaced Mark Word 不為 null,則利用 CAS 指令將對(duì)象頭的 mark word 恢復(fù)成為 Displaced Mark Word。如果成功,則 continue,否則膨脹為重量級(jí)鎖
重量級(jí)鎖:
利用的是 JVM 的監(jiān)視器(Monitor)
java 會(huì)為每個(gè) object 對(duì)象分配一個(gè) monitor,當(dāng)某個(gè)對(duì)象的同步方法(synchronized methods )被多個(gè)線程調(diào)用時(shí),該對(duì)象的 monitor 將負(fù)責(zé)處理這些訪問(wèn)的并發(fā)獨(dú)占要求。
- 當(dāng) Sychronized 修飾在代碼塊上的時(shí)候,使用的是 monitorenter 指令和 monitorexit 指令。
monitorenter
過(guò)程如下:
- 如果 Monitor 的進(jìn)入數(shù)為 0,則該線程進(jìn)入 Monitor,然后進(jìn)入數(shù)+1,然后該線程即為 Monitor 的所有者
- 如果線程已經(jīng)占有了 Monitor 只是重新進(jìn)入,則進(jìn)入數(shù)+1
- 如果其他線程占有了,則線程阻塞,直到 Monitor 的進(jìn)入數(shù)為 0,在嘗試獲取
monitorexit
過(guò)程如下:
- 指令執(zhí)行時(shí),Monitor 的進(jìn)入數(shù)減一,如果進(jìn)入數(shù)為 0,則線程退出 Monitor
- 其他被阻塞的線程可以嘗試獲取這個(gè) Monitor 的所有權(quán)
- Synchronize 作用在方式里時(shí),會(huì)加上一個(gè) ACC_SYNCHRONIZED 標(biāo)識(shí)。當(dāng)有這個(gè)標(biāo)識(shí)后,線程執(zhí)行將先獲取 Monitor,獲取成功才能執(zhí)行方法體。
三、AQS
// acquire方法獲取資源占有權(quán)
public final void acquire(int arg) {
/** 嘗試獲取,tryAcquire方法是子類必須實(shí)現(xiàn)的方法,
* 比如公平鎖和非公平鎖的不同就在于tryAcquire方法的實(shí)現(xiàn)的不同。
* 獲取失敗,則addWaiter方法,包裝node節(jié)點(diǎn),放入node雙向鏈表。再acquireQueued堵塞線程,循環(huán)獲取資源占有權(quán)。
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
private Node addWaiter(Node mode) {
//新構(gòu)建的node節(jié)點(diǎn),waitStatus初始值為0
Node node = new Node(Thread.currentThread(), mode);
//Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果尾部不為空,則說(shuō)明node雙向鏈表之前已經(jīng)被初始化了,那么直接把新node節(jié)點(diǎn)加入尾部
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果尾部為null,則說(shuō)明node雙向鏈表之前沒有被初始化,則,調(diào)用enq方法,初始化node雙向鏈表,并且把新節(jié)點(diǎn)加入尾部
enq(node);
return node;
}
acquire 方法總結(jié):
如果獲取成功:則 state 加 1,并調(diào)用 AQS 的父類
AbstractOwnableSynchronizer 的設(shè)置獨(dú)占線程,把當(dāng)前獨(dú)占線程設(shè)置當(dāng)前線程。
如果調(diào)用失?。簞t說(shuō)明,前面已經(jīng)有線程占用了這個(gè)資源,需要等待的線程釋放。則把當(dāng)前線程封裝成 node 節(jié)點(diǎn),放入 node 雙向鏈表,之后 Locksupport.pack()堵塞當(dāng)前線程。假如這個(gè)線程堵塞后被喚醒,則繼續(xù)循環(huán)調(diào)用 tryAcquire 方法獲取資源許可,獲取到了,則把自身 node 節(jié)點(diǎn)設(shè)置為 node 鏈表的頭節(jié)點(diǎn),把之前的頭節(jié)點(diǎn)去掉。
node 節(jié)點(diǎn)的 waitStatus 為 signal,則意味這其 next 節(jié)點(diǎn)可以被喚醒。
release 方法總結(jié):
如果線程釋放資源,調(diào)用 release 方法,release 方法會(huì)調(diào)用 tryRelease 方法嘗試釋放資源,如果釋放成功,tryRelease 方法會(huì)將 state 減 1,再調(diào)用 AQS 的父類
AbstractOwnableSynchronizer 的設(shè)置獨(dú)占線程為 null,再 locksupport.unpack()雙向 node 鏈表的頭 node 節(jié)點(diǎn)的線程,恢復(fù)其執(zhí)行。
四、實(shí)戰(zhàn)
順序打印 ABC。
/**
* @description:
* @author: mmc
* @create: 2020-01-03 09:42
**/
public class ThreadABC {
private static Object A = new Object();
private static Object B = new Object();
private static Object C = new Object();
private static class ThreadPrint extends Thread{
private String name;
private Object prev;
private Object self;
public ThreadPrint(String name,Object prev,Object self){
this.name=name;
this.prev=prev;
this.self=self;
}
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (prev) {
synchronized (self) {
System.out.println(name);
self.notifyAll();
}
try {
if(i>=9){
prev.notifyAll();
}else {
prev.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadPrint threadA = new ThreadPrint("A",C,A);
ThreadPrint threadB = new ThreadPrint("B",A,B);
ThreadPrint threadC = new ThreadPrint("C",B,C);
threadA.start();
Thread.sleep(10);
threadB.start();
Thread.sleep(10);
threadC.start();
}
}