@
目錄- 1、JUC 簡介
- 2、執行緒和行程
- 3、并非與并行
- 4、執行緒的狀態
- 5、wait/sleep的區別
- 6、Lock 鎖(重點)
- 1、Lock鎖
- 2、公平非公平:
- 3、ReentrantLock 構造器
- 4、Lock 鎖實作步驟:
- 7、synchronized 和 lock 鎖的區別
- 8、生產者和消費者問題(通信問題)
- 1、Synchronized 版本
- 2、JUC 版本
- 9、八個有關鎖的問題
- 關于鎖的八個問題
- 問題1:兩個同步方法,先執行發短信還是打電話?
- 問題2:如果發短信延遲2秒,誰先執行
- 問題3 加上一個沒有鎖的普通方法,誰先執行
- 問題4:兩個物件,一個呼叫發短信,一個呼叫打電話,誰先執行
- 問題5:原來的兩個同步方法,變為靜態同步方法,一個物件呼叫,誰先執行
- 問題6:創建兩個實體,呼叫兩個靜態同步方法,誰先執行
- 問題7:一個靜態同步方法、一個同步方法、一個物件呼叫,誰先執行
- 問題8:兩個物件,一個呼叫靜態同步方法,一個呼叫普通同步方法,誰先執行
- 小結
- 10、集合類的安全問題
- 1、List 不安全
- 2、Set 不安全
- 11、Callable(簡單)
- 12、JUC 常用輔助類
- 1、CountDownLatch
- 2、CyclickBarrier
- 3、Semaphore
- 13、ReadWriteLock 讀寫鎖
- 14、阻塞佇列
- 1、Blockqueue
- 2、SynchronizedQueue
- 15、執行緒池(重點)
- 1、執行緒池:三大方法
- 2、執行緒池:七大引數
- 3、四大拒絕策瑜:
- 16、為什么要使用執行緒池?
- 17、執行緒池執行緒復用的原理是什么?
- 18、AQS的理解
- 1、ReentrantLock和AQS的關系
- 2、ReentrantLock加鎖和釋放鎖的底層原理
- 19、執行緒創建的三種方式
- 20、為什么啟動start(),就呼叫run方法
- 21、執行緒的生命周期
- 21、執行緒安全:
- 執行緒安全解決問題方案:
- 1、互斥阻塞同步:也就是加鎖sychronized和ReenrtrantLock,加鎖優缺點?
- 22、執行緒同步機制
- 23、run()方法和sart()方法有什么區別
- 24、執行緒是否可以被重復啟動
- 25、volatile
- 26、java多執行緒之間的三種通信方式
- 1、synchronized來保證執行緒安全
- 2、通過Lock()
- 3、BlockingQueue
- 27、說一說synchronized的底層實作原理
- 28、CAS
- 1、概念
- 2、CAS可能產生ABA問題:
- 29、鎖升級初步
- 1、偏向鎖:
- 2、輕量級鎖
- 3、鎖重入鎖
- 4、自旋鎖什么時候升級為重量級鎖
- 5、為什么有自旋鎖還需要重量級鎖
- 6、偏向鎖是否一定比自旋鎖效率高
- 30、ThreadLocal機制
- 31、ThreadLocal機制的記憶體泄露
- 留言:
多執行緒JUC并發篇
1、JUC 簡介
什么是 JUC ?
-
JUC 就是 java.util.concurrent 下面的類包,專門用于多執行緒的開發

為什么使用 JUC ? -
以往我們所學,普通的執行緒代碼,都是用的thread或者runnable介面
-
但是相比于callable來說,thread沒有回傳值,且效率沒有callable高
2、執行緒和行程
-
行程就是一個應用程式
-
執行緒是行程中的一個物體,執行緒本身是不會獨立存在的,
行程是代碼在資料集合上的一次運行活動, 是系統進行資源分配和調度的基本單位,
執行緒則是行程的一個執行路徑, 一個行程中至少有一個執行緒,行程中的多個執行緒共享行程的資源,
? 作業系統在分配資源時是把資源分配給行程的, 但是CPU 資源比較特殊, 它是被分配到執行緒的, 因為真正要占用CPU 運行的是執行緒, 所以也說執行緒是CPU 分配的基本單位,
? java默認有幾個執行緒? 兩個 main執行緒 gc執行緒
? Java 中,使用 Thread、Runnable、Callable 開啟執行緒,
? Java 沒有權限開啟執行緒 、Thread.start() 方法呼叫了一個 native 方法 start0(),它呼叫了底層 C++ 代碼,
3、并非與并行
并發多執行緒操作同一個資源,交替執行
- CPU一核, 模擬出來多條執行緒,天下武功,唯快不破,快速交替
并行(多個人一起行走, 同時進行)
- CPU多核,多個執行緒同時進行 ; 使用執行緒池操作
4、執行緒的狀態
-
新建
-
就緒
-
阻塞
-
運行
-
死亡
5、wait/sleep的區別
-
來自不同的類:wait來自object類, sleep來自執行緒類
-
關于鎖的釋放:wait會釋放鎖, sleep不會釋放鎖
-
使用范圍不同:wait必須在同步代碼塊中,sleep可以在任何地方睡
-
是否需要捕獲例外:wait不需要捕獲例外,sleep需要捕獲例外
6、Lock 鎖(重點)
Synchronized 傳統的鎖
之前我們所學的使用執行緒的傳統思路是:
-
單獨創建一個執行緒類,繼承Thread或者實作Runnable
-
在這個執行緒類中,重寫run方法,同時添加相應的業務邏輯
-
在主執行緒所在方法中new上面的執行緒物件,呼叫start方法啟動
1、Lock鎖
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aFbI65Pz-1650013582522)(https://pizximfzuc.feishu.cn/space/api/box/stream/download/asynccode/?code=Nzg2NDU1MDkyYTk1YWVlZDJjMzM3M2QxODNlMWM4NWRfeUZORjNjcWtmd3ZrR1FmWEZ2MkVWdjZMWGtHenpsM3JfVG9rZW46Ym94Y25qVzdjWERnR2owZVlMRXU4S3pTT1VjXzE2NTAwMTM0OTI6MTY1MDAxNzA5Ml9WNA)]](https://img-blog.csdnimg.cn/e48a1dcbfe9a4a67aeb1716563b60e9c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6K-X6aOO6ZuF6Z-1,size_20,color_FFFFFF,t_70,g_se,x_16)
可以看到,
Lock是一個介面,有三個實作類,現在我們使用
ReentrantLock 就夠用了
查看
ReentrantLock 原始碼,構造器
2、公平非公平:
-
公平鎖::十分公平, 可以先來后到,一定要排隊
-
非公平鎖::十分不公平,可以插隊(默認)
3、ReentrantLock 構造器
-
ReentrantLock 默認的構造方法是非公平鎖(可以插隊),
-
如果在構造方法中傳入 true 則構造公平鎖(不可以插隊,先來后到),
4、Lock 鎖實作步驟:
- 創建鎖,new ReentrantLock()
- 加鎖,lock.lock()
- 解鎖,lock.unlock()
- 基本結構固定,中間的業務自己靈活修改
7、synchronized 和 lock 鎖的區別
-
synchronized 是內置的 Java 關鍵字,Lock 是一個 Java 類
-
synchronized 無法判斷獲取鎖的狀態,Lock可以判斷是否獲取到了鎖
-
synchronized 會自動釋放鎖,Lock 必須要手動釋放鎖!如果不釋放鎖,會產生死鎖
-
synchronized 假設執行緒1(獲得鎖,然后發生阻塞),執行緒2(一直等待); Lock 鎖就不一定會等待下去,可使用 tryLock 嘗試獲取鎖
-
synchronized 可重入鎖,不可以中斷的,非公平的;Lock鎖,可重入的,可以判斷鎖,是否公平(可自己設定)
-
synchronized 適合鎖少量的代碼同步問題,Lock 適合鎖大量的同步代碼
總體來說,synchronized 本來就是一個關鍵字,很多規則都是定死的,靈活性差;Lock 是一個類,靈活性高
8、生產者和消費者問題(通信問題)
1、Synchronized 版本
解決執行緒之間的通信問題,比如執行緒操作一個公共的資源類
基本流程可以總結為:
-
等待:判斷是否需要等待
-
業務:執行相應的業務
-
通知:執行完業務通知其他執行緒
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = https://www.cnblogs.com/zbqblogs/archive/2022/04/16/new Data();
// 創建一個生產者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// 創建一個消費者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//這是一個緩沖類,生產和消費之間的倉庫,公共資源類
class Data{
// 這是倉庫的資源,生產者生產資源,消費者消費資源
private int num = 0;
// +1,利用關鍵字加鎖
public synchronized void increment() throws InterruptedException {
// 首先查看倉庫中的資源(num),如果資源不為0,就利用 wait 方法等待消費,釋放鎖
if(num!=0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他執行緒 +1 執行完畢
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 首先查看倉庫中的資源(num),如果資源為0,就利用 wait 方法等待生產,釋放鎖
if(num==0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他執行緒 -1 執行完畢
this.notifyAll();
}
}
思考問題:如果存在ABCD4個執行緒是否安全?
- 不安全,會有虛假喚醒
查看 api 檔案

解決辦法:if 判斷改為 while,防止虛假喚醒
-
因為 if 只會執行一次,執行完會接著向下執行 if() 外邊的代碼
-
而 while 不會,直到條件滿足才會向下執行 while() 外邊的代碼
修改代碼為:
// ...
// 使用 if 存在虛假喚醒
while (num!=0){
this.wait();
}
// ...
while(num==0){
this.wait();
}
2、JUC 版本
鎖、等待、喚醒 都進行了更換

改造之后,確實可以實作01切換,但是ABCD是無序的,不滿足我們的要求,
Condition 的優勢在于,精準的通知和喚醒執行緒!比如,指定通知下一個進行順序,
重新舉個例子,
三個執行緒 A執行完呼叫B,B執行完呼叫C,C執行完呼叫A,分別用不同的監視器,執行完業務后指定喚醒哪一個監視器,實作執行緒的順序執行
鎖是統一的,但監視器是分別指定的,分別喚醒,signal,之前使用的是 signalAll
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
while (num != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + " Im A ");
num = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + " Im B ");
num = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + " Im C ");
num = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
9、八個有關鎖的問題
深入理解鎖
關于鎖的八個問題
問題1:兩個同步方法,先執行發短信還是打電話?
經過測驗,一直是先發短信
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rsXbJVS7-1650013582524)(https://pizximfzuc.feishu.cn/space/api/box/stream/download/asynccode/?code=NzExNGI3OTliYmQ5MTk5ZmVjMjYzNzAxM2MwYmQwMGJfM3l3b1NKWndKYVA5cjRrVHc4SWdxektzOGQxSmZKeHVfVG9rZW46Ym94Y25nTkhuUVdVdllwVWhiYVZXS09Oak0xXzE2NTAwMTM0OTI6MTY1MDAxNzA5Ml9WNA)]](https://img-blog.csdnimg.cn/f61626937ffa42a6b716db1992615546.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6K-X6aOO6ZuF6Z-1,size_18,color_FFFFFF,t_70,g_se,x_16)
問題2:如果發短信延遲2秒,誰先執行
結果依舊是先發短信,后打電話
分析:
-
并不是由于發短信在前導致的
-
本案例中,方法前加synchronized,鎖的其實該方法的呼叫者,也就是 phone 實體,兩個方法共用同一個 phone 物件的鎖,誰先拿到,誰先執行
-
在主執行緒中,先呼叫發短信,所以先執行,打電話等釋放鎖再執行
問題3 加上一個沒有鎖的普通方法,誰先執行
觀察發現,先執行了 hello
分析原因:
- hello 是一個普通方法,不受 synchronized 鎖的影響,不用等待鎖釋放,
問題4:兩個物件,一個呼叫發短信,一個呼叫打電話,誰先執行
結論,先打電話,后發短信
分析原因:
- 兩個物件兩把鎖,互不影響,1拿到鎖還需要等待3秒,2拿到物件立刻就能打電
問題5:原來的兩個同步方法,變為靜態同步方法,一個物件呼叫,誰先執行
結果,始終是先發短信,后打電話
分析原因:
靜態方法前面加鎖,鎖的其實是這個方法所在的Class類物件(非靜態那個是實體物件,注意區分)
Class類物件也是全域唯一,使用的是通一把鎖,所以先發短信,后打電話
雖然和上面的實體物件都是對應了全域唯一的鎖,但原理還是有所不同
主執行緒先執行了發短信,打電話就必須等鎖釋放再執行
問題6:創建兩個實體,呼叫兩個靜態同步方法,誰先執行
結果,現發短信,后打電話
原因分析:
- 雖然實體物件是兩個,但是兩個靜態同步方法對應的鎖是Class類物件的鎖,還是全域唯一
問題7:一個靜態同步方法、一個同步方法、一個物件呼叫,誰先執行
結果:先打電話,后發短信
原因分析:
-
靜態同步方法和普通同步方法分別對應了不同的鎖,互不干擾
-
發短信需要延遲3秒,所以打電話先執行了
問題8:兩個物件,一個呼叫靜態同步方法,一個呼叫普通同步方法,誰先執行
結果,先打電話,后發短信
分析原因:
同問題7相同,兩個方法對應了不同的鎖,互不干擾
發短信還需要等待3秒,所以打電話先執行完了
小結
無外乎兩種鎖,一個是new實體的鎖,一個是Class物件的鎖
實體的鎖,與當前的實體唯一對應,Class物件的鎖與這個類唯一對應
如果兩個方法等同一個鎖,必須一個先執行完,釋放鎖,另一個才可以執行
如果兩個方法等不同的鎖,互不影響,誰先誰后看具體情況
在主執行緒中,代碼是順序執行的,再結合鎖的原理,綜合判斷執行緒執行的順序
10、集合類的安全問題
在 JUC 并發編程情況下,適用于單執行緒的集合類將出現并發問題
1、List 不安全
運行出現并發修改例外,
java.util.ConcurrentModificationException
解決方案:
解決方案1:
-
ArrayList 換成 Vector,Vector 方法里加了鎖
-
Vector出現比較早,由于鎖導致方法執行效率太低,不推薦使用
解決方案2:
-
使用 Collection 靜態方法,回傳一個帶鎖的 List 實體
List
list = Collections.synchronizedList(new ArrayList<>());
解決方案3:
-
使用 JUC 提供的適合并發使用的 CopyOnWriteArrayList
List
list = new CopyOnWriteArrayList<>();
分析:
CopyOnWrite 表示寫入時復制,簡稱COW,計算機程式設計領域的一種優化策略
多執行緒呼叫list時,讀取時沒有問題,寫入的時候會復制一份,避免在寫入時被覆寫
這也是一種讀寫分離的思想
CopyOnWriteArrayList 比 Vector 強在哪里?前者是寫入、復制,且使用 lock 鎖,效率比 Vector 的synchronized 鎖要高很多
2、Set 不安全
Set 和 List 同理可得:多執行緒情況下,普通的 Set 集合是執行緒不安全的
-
使用 Collection 工具類的 synchronized 包裝的 Set 類
Set
set = Collections.synchronizedSet(new HashSet<>());
-
使用 JUC 提供的 CopyOnWriteArraySet 寫入復制
Set
set = new CopyOnWriteArraySet<>();
思考,HashSet 底層到底是什么?
- hashSet底層就是一個HashMap;hashSet只使用了hashMap的key
11、Callable(簡單)
得到的資訊:
可以有回傳值
可以拋出例外
方法不同,run() => call()
使用時注意
-
Callable 的泛型也是 call 方法的回傳值型別
-
Callable 的實作類無法直接放在 Thread 中,還需要先放在 -
-
FutureTask 中,再放在 Thread 中FutureTask 就相當于適配類,起到牽線的作用
注意:
-
運行結果會產生快取,目的是為了提高效率
-
get方法可能會產生阻塞,所以放在了最后
12、JUC 常用輔助類
1、CountDownLatch
減法計數器
原理:
countDownLatch.countDown(); //數量減1
countDownLatch.await();// 等待計數器歸零,然后再向下執行
每次有執行緒呼叫countDown()數量-1,假設計數器變為0,countDownLatch.await();就會被喚醒,繼續執行
2、CyclickBarrier
加法計數器,與 CountDownLatch 正好相反
相當于設定一個目標,執行緒數達到目標值之后才會執行
3、Semaphore
計數信號量,比如說,有6輛車,3個停車位,汽車需要輪流等待車位
常用在需要限流的場景中,
原理:
-
*semaphore.acquire() 獲得資源,如果資源已經使用完了,就等待資源釋放后再進行使用!
-
*semaphore.release() 釋放,會將當前的信號量釋放+1,然后喚醒等待的執行緒!
用途:
-
*多個共享資源互斥的使用!
-
*并發限流,控制最大的執行緒數!
13、ReadWriteLock 讀寫鎖

ReadWriteLock,這是一個更加細粒度的鎖
// 自定義快取
class MyCache{
private volatile Map<String,String> map = new HashMap<>();
private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
// 存,寫,寫入的時候只希望只有一個執行緒在寫
public void write(String key, String value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "執行緒開始寫入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "執行緒開始寫入ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
// 取,讀,所有執行緒都可以讀
public void read(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "執行緒開始讀取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "執行緒讀取ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
小結:
- 讀-讀 可以共存
- 讀-寫 不能共存
- 寫-寫 不能共存
也可以這樣稱呼,含義都是一樣,名字不同而已
- 獨占鎖(寫鎖)一次只能由一個執行緒占有
- 享鎖(讀鎖)一次可以有多個執行緒占有
14、阻塞佇列
1、Blockqueue
阻塞佇列 BlockQueue 是Collection 的一個子類
應用場景:多執行緒并發處理、執行緒池
BlockingQueue 有四組 API
方式 拋出例外 不會拋出例外,有回傳值 阻塞等待 超時等待
添加操作 add() offer() 供應 put() offer(obj,int,timeunit.status)可設定時間
移除操作 remove() poll() 獲得 take() poll(int,timeunit.status)可設定時間
判斷佇列首部 element() peek() 偷看,偷窺 SynchronizedQueue 同步佇列
同步佇列沒有容量,進去一個元素,必須等待取出來之后,才能再往里面放一個元素
2、SynchronizedQueue
- SynchronizedQueue使用 put 方法和 take 方法
- Synchronized 和 其他的 BlockingQueue 不一樣 它不存盤元素;
- put了一個元素,就必須從里面先 take 出來,否則不能再 put 進去值!
- 并且 SynchronousQueue 的 take 是使用了 lock 鎖保證執行緒安全的,
15、執行緒池(重點)
池化技術
執行緒池重點:三大方式、七大引數、四種拒絕策略
程式的運行的本質:占用系統的資源 ! 優化CPU資源的使用 ===>池化技術(執行緒池、連接池、記憶體池、物件池…)
池化技術:實作準備好一些資源,有人要用,就來我這里拿,用完之后還給我
執行緒池的好處:
- 降低資源消耗
- 提高回應速度
- 方便管理
如何優化:
- 執行緒復用,可以控制最大并發數,管理執行緒
1、執行緒池:三大方法
查看阿里巴巴開發手冊

- ExecutorService threadPool = Executors.newSingleThreadExecutor();//單個執行緒
- ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //創建一個固定的執行緒池的大小
- ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸縮的(不會出現OOM)
之前我們所學知識,直接創建執行緒,現在我們通過執行緒池來創建執行緒,使用池化技術
> ExecutorService service = Executors.newCachedThreadPool();//可伸縮的,遇強則強,遇弱則弱
> try {
> for (int i = 0; i < 10; i++) {
> service.execute(() -> {
> System.out.println(Thread.currentThread().getName() + "ok");
> });
> }
> //執行緒池用完要關閉執行緒池
> } finally {
> service.shutdown();
> }
2、執行緒池:七大引數
public ThreadPoolExecutor(int corePoolSize,//核心執行緒數 也就是一直作業的執行緒數量
int maximumPoolSize,//最大執行緒數,如果核心心執行緒數使用完
long keepAliveTime,//非核心執行緒的存活時間
TimeUnit unit,//非核心執行緒的存活時間單位
BlockingQueue<Runnable> workQueue,//阻塞佇列
ThreadFactory threadFactory,//執行緒工廠
RejectedExecutionHandler handler) //拒絕策略
提交優先級
execute()提交方法中原始碼中的幾個if里面都會呼叫執行方法addWorker(Rannale firstTask,boolean core )

執行優先級


執行優先級:
addWorker(Rannale firstTask,boolean core )
submit()與execute()區別
1、submit()有回傳值,execute()沒有回傳值
2、submit()方法里面呼叫了execute()方法
3、四大拒絕策瑜:

16、為什么要使用執行緒池?
為了減少創建和銷毀執行緒的次數,讓每個執行緒可以多次使用,可根據系統情況調整執行的執行緒數量,防止消耗過多記憶體,所以我們可以使用執行緒池.
17、執行緒池執行緒復用的原理是什么?
首先執行緒池內的執行緒都被包裝成了一個個的java.util.concurrent.ThreadPoolExecutor.Worker,然后這個worker會馬不停蹄的執行任務,執行完任務之后就會在while回圈中去取任務,取到任務就繼續執行,取不到任務就跳出while回圈(這個時候worker就不能再執行任務了)執行 processWorkerExit方法,這個方法呢就是做清場處理,將當前woker執行緒從執行緒池中移除,并且判斷是否是例外的進入processWorkerExit方法,如果是非例外情況,就對當前執行緒池狀態(RUNNING,shutdown)和當前作業執行緒數和當前任務數做判斷,是否要加入一個新的執行緒去完成最后的任務(防止沒有執行緒去做剩下的任務).
那么什么時候會退出while回圈呢?取不到任務的時候(getTask() == null)
/java/util/concurrent/ThreadPoolExecutor.java:1127
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {...執行任務...}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//(rs == SHUTDOWN && workQueue.isEmpty()) || rs >=STOP
//若執行緒池狀態是SHUTDOWN 并且 任務佇列為空,意味著已經不需要作業執行緒執行任務了,執行緒池即將關閉
//若執行緒池的狀態是 STOP TIDYING TERMINATED,則意味著執行緒池已經停止處理任何任務了,不在需要執行緒
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
//把此作業執行緒從執行緒池中洗掉
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//allowCoreThreadTimeOut:當沒有任務的時候,核心執行緒數也會被剔除,默認引數是false,官方推薦在創建執行緒池并且還未使用的時候,設定此值
//如果當前作業執行緒數 大于 核心執行緒數,timed為true
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//(wc > maximumPoolSize || (timed && timedOut)):當作業執行緒超過最大執行緒數,或者 允許超時并且超時過一次了
//(wc > 1 || workQueue.isEmpty()):作業執行緒數至少為1個 或者 沒有任務了
//總的來說判斷當前作業執行緒還有沒有必要等著拿任務去執行
//wc > maximumPoolSize && wc>1 : 就是判斷當前作業執行緒是否超過最大值
//或者 wc > maximumPoolSize && workQueue.isEmpty():作業執行緒超過最大,基本上不會走到這,
// 如果走到這,則意味著wc=1 ,只有1個作業執行緒了,如果此時任務佇列是空的,則把最后的執行緒洗掉
//或者(timed && timedOut) && wc>1:如果允許超時并且超時過一次,并且至少有1個執行緒,則洗掉執行緒
//或者 (timed && timedOut) && workQueue.isEmpty():如果允許超時并且超時過一次,并且此時作業 佇列為空,那么妥妥可以把最后一個執行緒(因為上面的wc>1不滿足,則可以得出來wc=1)洗掉
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
//如果減去作業執行緒數成功,則回傳null出去,也就是說 讓作業執行緒停止while輪訓,進行收尾
return null;
continue;
}
try {
//判斷是否要阻塞獲取任務
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
18、AQS的理解
1、ReentrantLock和AQS的關系
java并發包下的很多API都是基于AQS來實作加鎖和釋放等功能,AQS是并java發包的基礎類,
舉個列子,ReentrantLock、ReentrantReadWr
iteLock底層都是基于AQS來實作的,ReentrantLock內部包含已個AQS物件,
AQS的全稱是AbstractQueueSynchonizer,抽象佇列同步鎖,
2、ReentrantLock加鎖和釋放鎖的底層原理
如果現在有一個執行緒過來嘗試用ReentrantLock的lock()方法進行加鎖,會發生什么?
很簡單,這個AQS物件內部有一個核心的變數state,是int型別的,代表加鎖的狀態,初始情況下為0.
另外AQS內部還有一個關鍵變數,用來記錄加鎖執行緒是哪個執行緒,初始化狀態下,這個執行緒是null,
[MISSING IMAGE: image-20220324164210432, image-20220324164210432 ]
接著執行緒1跑過來會呼叫ReentrantLock的lock()方法嘗試加鎖,這個加鎖的程序,是直接用CAS操作將state值進行0->1的,如果之間前沒有人加過鎖,那么state為0,此時執行緒1加鎖成功
一旦執行緒加鎖成功后,就可以設定當前執行緒就是自己,
下圖就是執行緒1的加鎖程序
其實到這就知道了AQS就是并發包里的一個核心組件,里面有state變數,加鎖 執行緒變數等核心東西,維護了加鎖狀態,你會發現ReentrantLock就是一個外面的API,內部的核心鎖機制都是依賴AQS組件的
這個ReentrantLock之所以以Reentrant開頭,意思是它可以可重入鎖,
可重入鎖的意思是,就是你可以對ReentrantLock物件多次執行lock()加鎖和unlock()釋放鎖,也就是可以對一個鎖加多次,叫做可重入加鎖
明白這個后看這個state變數,其實每次執行緒1可重入加鎖一次,那么他會判斷當前執行緒就是自己,那么他自己就可以沖重入多次加鎖,每次都state+1,別的沒有變化,
執行緒1加鎖完成后,那么執行緒2跑來加鎖會發生什么呢?
我們看看互斥鎖怎么實作的,執行緒2跑來發現state不是0,所以CAS重0->1就失敗,因為不為0說明被加鎖鎖了,那么就會去看當前加鎖執行緒是否是自己,不是的話自己就加鎖失敗,
看圖示意:
[MISSING IMAGE: image-20220324170008447, image-20220324170008447 ]
接著,執行緒2就會將自己放入到AQS的一個等待佇列,因為自己嘗試加鎖失敗了,此時就要將自己放入佇列中等待,等待執行緒1釋放鎖之后,自己就可以重新嘗試加鎖了,
能夠看到,AQS是如此的核心,AQS內部還有一個等待u佇列,專門放哪些加鎖失敗的執行緒,
[MISSING IMAGE: image-20220324170416331, image-20220324170416331 ]
接著,執行緒1在執行完自己的業務后,就會釋放鎖!他釋放鎖的程序很簡單,就是將AQS內部的state變數值遞減到,將“加鎖執行緒”也是設定為null,徹底釋放鎖了,
接下來,會從等待佇列中喚醒對頭的執行緒2,執行緒2重新嘗試加鎖,還是用CAS將state變為1,當前執行緒為自己執行緒,同時執行緒2自己就可以出隊了,
19、執行緒創建的三種方式
-
1、Thread類
是Java中表示執行緒的類,里面包含了一個執行緒運行程序中方法run()方法
使用Thread類創建并啟動執行緒的步驟:
1.寫一個類,繼承Thread類
2.覆寫Thread類中的run()方法
3.創建執行緒的實體物件
4.呼叫執行緒的實體物件的start()方法去啟動執行緒
注意:
1.啟動執行緒呼叫start()方法
2.啟動執行緒之后會自動呼叫run()方法
3.需要執行緒完成某件事情,將對應的代碼添加到run()方法中即可
-
2、實作Runnale介面(推薦使用)
步驟:
-
寫一個類實作Runnable介面
-
實作Runnable介面中的run()
-
創建Thread類創建物件,并將第三步創建的物件作為引數傳遞到構造方法中
-
呼叫Thread創建物件的start()方法,啟動執行緒
采用匿名內部類方式創建執行緒(固定格式)
public static void main(String[] args) {
Thread th = new Thread() {
public void run() {
System.out.println("匿名內部類的run方法");
};
};
th.start();
}
-
3、實作Callable介面,并與future
-
4、執行緒池結合使用
20、為什么啟動start(),就呼叫run方法
關注原始碼可以發現,在start()方法中,默認呼叫了一個JNI方法,這個方法是java平臺用于和本地C代碼進行相互操作的API
21、執行緒的生命周期
執行緒的生命周期就是執行緒的狀態
- 1新建狀態 new
當使用new關鍵字創建執行緒實體后,該執行緒就屬于新建狀態,但是不會執行
- 2、就緒狀態Runnable
當呼叫start()方法時,該執行緒處于就緒狀態,表示可以執行,但是不一定會立即執行,而是等待cpu
分配時間片進行處理
- 3、運行狀態(Running)
當為該執行緒分配到時間片后,執行該方法的run方法,就處于運行狀態
- 4、暫停狀態(包括休眠、等待、阻塞等)(Block)
當執行緒呼叫sleep()方法,主動放棄CPU資源,或者執行緒吊用阻塞IO方法時,比如控制臺的Scannner輸入方法
- 死亡狀態(dead)
當執行緒的run()方法執行完成之后就處于死亡狀態
注意:
1.當執行緒創建時,并不會立即執行,需要呼叫start方法,使其處于就緒狀態
2.執行緒處于就緒狀態時,也不會立即執行執行緒,需要等待CPU分配時間
3.當執行緒阻塞時,會讓出占有的CPU,當阻塞結束時,執行緒會進入就緒狀態,重新等待CPU,而不是直接進入到運行狀態
-
Thread.yield():
讓出當前CPU時間片,該執行緒會從運行狀態進入到就緒狀態,此時繼續與其他執行緒搶占CPU
-
Thread.sleep(time):
讓執行緒休眠time毫秒,該執行緒會從運行狀態進入到阻塞狀態,不會與其他執行緒搶占CPU,當time毫秒過后,該執行緒會從阻塞狀態進入到就緒狀態,重新與其他執行緒搶占CPU
異步的效率會比同步的高,但是異步存在資料安全問題
多執行緒并發執行,也就是執行緒異步處理,并發執行存在執行緒安全問題
21、執行緒安全:
在實際開發中,使用多執行緒程式的情況很多,如銀行排號系統、火車站售票系統等,這種多執行緒的程式通常會發生問題,以火車站售票系統為例,在代碼中判斷當前票數是否大于0,如果大于0則執行將該票出售給乘客的功能,但當兩個執行緒同時訪問這段代碼時(假如這時只剩下一張票),第一個執行緒將票售出,與此同時第二個執行緒也已經執行完成判斷 是否有票的操作, 并得出結論票數大于0,于是它也執行售出操作,這樣就會產生負數,所以在撰寫多執行緒程式時,應該考慮到錢程安全問題,實質上執行緒安全問題來源于兩個執行緒同時存取單一物件的資料,
執行緒安全解決問題方案:
1、互斥阻塞同步:也就是加鎖sychronized和ReenrtrantLock,加鎖優缺點?
22、執行緒同步機制
為了避免多執行緒的安全問題,需要在公共訪問的內容上加鎖,加鎖之后,當一個執行緒執行該內容時,其他執行緒無法執行該內容,只有當該執行緒將此部分內容執行完了之后,其他執行緒才可以執行,
- 1.找到多執行緒公共執行的內容
- 2.在此內容上合適的位置加上鎖
鎖:
1、****synchronized可以加在方法上,也可以加在代碼塊中
加在方法上,在回傳值前面加synchronized既可,
比如:public synchronized void run() {}表示給run方法整體加上了鎖,
加在代碼塊上:
synchronized(this) {
//需要同步執行的代碼
}
注意:加鎖之后,被加鎖的代碼就變成了同步,會影響效率,所以應該盡量減小加鎖的范圍
2、也可以用RantantLock
23、run()方法和sart()方法有什么區別
run()方法是執行緒的執行體,他的方法代表執行緒需要完成的任務,而start()方法用來啟動執行緒,
24、執行緒是否可以被重復啟動
25、volatile
26、java多執行緒之間的三種通信方式
1、synchronized來保證執行緒安全
如果執行緒之間是通過synchronized來保證執行緒安全,則可以利用wait()、notify()、notifyAll()來實作通信
2、通過Lock()
如果執行緒之間是通過Lock()來保證執行緒安全的,則可以利用await()、signal()、signalAll()來說實作執行緒通信
這三個方法都是Condition介面中的方法,
3、BlockingQueue
jdk1.5中提供了BlockingQueue介面,雖然四Queue的子介面,但是主要用途并不是作為容器,而是作為執行緒的通信工具,BlockingQueue具有一個特征:當生產者執行緒試圖向BlockingQueue中放入一個元素,如果該佇列已滿,則該執行緒阻塞;
27、說一說synchronized的底層實作原理
一、synchronized作用在代碼塊時,它的底層是通過monitorenter、monitorexit指令來實作的,
-
*monitorenter:
每個物件都是一個監視器鎖(monitor),當monitor被占用時就會處于鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,程序如下:
如果monitor的進入數為0,則該執行緒進入monitor,然后將進入數設定為1,該執行緒即為monitor的所有者,如果執行緒已經占有該monitor,只是重新進入,則進入monitor的進入數加1,如果其他執行緒已經占用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權,
-
*monitorexit:
執行monitorexit的執行緒必須是objectref所對應的monitor持有者,指令執行時,monitor的進入數減1,如果減1后進入數為0,那執行緒退出monitor,不再是這個monitor的所有者,其他被這個monitor阻塞的執行緒可以嘗試去獲取這個monitor的所有權,
monitorexit指令出現了兩次,第1次為同步正常退出釋放鎖,第2次為發生異步退出釋放鎖,
二、方法的同步并沒有通過 monitorenter 和 monitorexit 指令來完成,不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符,JVM就是根據該標示符來實作方法的同步的:
當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor,在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件,
三、總結
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實作,無需通過位元組碼來完成,兩個指令的執行是JVM通過呼叫作業系統的互斥原語mutex來實作,被阻塞的執行緒會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響
28、CAS
1、概念
2、CAS可能產生ABA問題:
ABA解決問題:加一個版本號
版本號:數值型或者布爾型
29、鎖升級初步
- new->偏向鎖->輕量級鎖(無鎖、自旋鎖、自適應自旋3)->重量級鎖
1、偏向鎖:
在鎖物件的物件頭中記錄?下當前獲取到該鎖的執行緒ID,該執行緒下次如果?來獲取該鎖就
2、輕量級鎖
由偏向鎖升級?來,當?個執行緒獲取到鎖后,此時這把鎖是偏向鎖,此時如果有第?個 執行緒來競爭鎖,偏向鎖就會升級為輕量級鎖,之所以叫輕量級鎖,是為了和重量級鎖區分開來,輕 量級鎖底層是通過?旋來實作的,并不會阻塞執行緒
3、鎖重入鎖
sychnronized必須記錄重入次數,因為要解鎖必須對應次數
偏向鎖 自旋鎖 ->執行緒堆疊->LR+1
4、自旋鎖什么時候升級為重量級鎖
競爭加劇:有執行緒超過10次,或者自旋鎖執行緒數超過CPU核數的一半,1.6以后加入自適應自旋,JVM自己控
5、為什么有自旋鎖還需要重量級鎖
自旋是消耗CPU資源的,如果時間過長或者自旋執行緒數多,CPU會被大量消耗
重量級鎖有等待佇列,所有拿不到鎖的進入等待佇列,不需要消耗CPU資源
6、偏向鎖是否一定比自旋鎖效率高
不一定,在明確知道會有多執行緒競爭的情況下,偏向鎖肯定會涉及鎖撤銷,這時候直接使用自旋鎖
JVM啟動程序,會有很多執行緒競爭(明確),所以默認情況下啟動是不會啟動偏向鎖,過一會時間再打開
30、ThreadLocal機制
對于ThreadLocal而言,常用的方法,就是get/set/initialValue方法,
ThreadLocal提供一個執行緒(Thread)區域變數,訪問到某個變數的每一個執行緒都擁有自己的區域變數,說白了,ThreadLocal就是想在多執行緒環境下去保證成員變數的安全,
你會看到,set需要首先獲得當前執行緒物件Thread;
然后取出當前執行緒物件的成員變數ThreadLocalMap;
如果ThreadLocalMap存在,那么進行KEY/VALUE設定,KEY就是ThreadLocal;
如果ThreadLocalMap沒有,那么創建一個;
說白了,當前執行緒中存在一個Map變數,KEY是ThreadLocal,VALUE是你設定的值,
31、ThreadLocal機制的記憶體泄露
首先來說,如果把ThreadLocal置為null,那么意味著Heap中的ThreadLocal實體不在有強參考指向,只有弱參考存在,因此GC是可以回收這部分空間的,也就是key是可以回收的,但是value卻存在一條從Current Thread過來的強參考鏈,因此只有當Current Thread銷毀時,value才能得到釋放,
因此,只要這個執行緒物件被gc回收,就不會出現記憶體泄露,但在threadLocal設為null和執行緒結束這段時間內不會被回收的,就發生了我們認為的記憶體泄露,最要命的是執行緒物件不被回收的情況,比如使用執行緒池的時候,執行緒結束是不會銷毀的,再次使用的,就可能出現記憶體泄露,
那么如何有效的避免呢?
事實上,在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那么是會對value置為null的,我們也可以通過呼叫ThreadLocal的remove方法進行釋放!
留言:
這是本人今年春招找實習作業準備總結,記錄在此,如有需要的老鐵可以看看,如有問題可以留言指導
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/458375.html
標籤:其他
上一篇:二叉樹與堆
