樹 樹的定義 樹是一種數據結構,樹結構只有一個根節點,除根節點外,其餘節點被分成M(M>0) 個互不相交的集合T1,T2,T3,......,Tm. 其中每一個集合Ti(1 < i < m)又是一顆與樹結構類似的子樹。每個子樹的根節點有且只有一個前驅,可以有0個或多個後繼。因此,樹是遞歸定義的。 如 ...
@
目錄- 1、JUC 簡介
- 3、並非與並行
- 4、線程的狀態
- 5、wait/sleep的區別
- 6、Lock 鎖(重點)
- 7、synchronized 和 lock 鎖的區別
- 8、生產者和消費者問題(通信問題)
- 9、八個有關鎖的問題
- 10、集合類的安全問題
- 11、Callable(簡單)
- 12、JUC 常用輔助類
- 13、ReadWriteLock 讀寫鎖
- 14、阻塞隊列
- 15、線程池(重點)
- 16、為什麼要使用線程池?
- 17、線程池線程復用的原理是什麼?
- 18、AQS的理解
- 19、線程創建的三種方式
- 20、為什麼啟動start(),就調用run方法
- 21、線程的生命周期
- 21、線程安全:
- 22、線程同步機制
- 23、run()方法和sart()方法有什麼區別
- 24、線程是否可以被重覆啟動
- 25、volatile
- 26、java多線程之間的三種通信方式
- 27、說一說synchronized的底層實現原理
- 28、CAS
- 29、鎖升級初步
- 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鎖
可以看到,
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 = 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:兩個同步方法,先執行發簡訊還是打電話?
經過測試,一直是先發簡訊
問題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方法進行釋放!
留言:
這是本人今年春招找實習工作准備總結,記錄在此,如有需要的老鐵可以看看,如有問題可以留言指導