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