網(wǎng)站建設(shè)程序源碼青島網(wǎng)站優(yōu)化公司
JUC
今天我們來進(jìn)入到 Java并發(fā)編程 JUC 框架的學(xué)習(xí) ,內(nèi)容比較多,但希望我們都能靜下心來,耐心的看完這篇文章
文章目錄
- JUC
- 進(jìn)程
- 概述
- 對(duì)比
- 線程
- 創(chuàng)建線程
- Thread
- Runnable
- Callable
- 線程方法
- API
- run start
- sleep yield
- join
- interrupt
- 打斷線程
- 打斷 park
- 終止模式
- daemon
- 不推薦
- 線程原理
- 運(yùn)行機(jī)制
- 線程調(diào)度
- 未來優(yōu)化
- 線程狀態(tài)
- 查看線程
- 同步
- 臨界區(qū)
- syn-ed
- 使用鎖
- 同步塊
- 同步方法
- 線程八鎖
- 鎖原理
- Monitor
- 字節(jié)碼
- 鎖升級(jí)
- 升級(jí)過程
- 偏向鎖
- 輕量級(jí)鎖
- 鎖膨脹
- 鎖優(yōu)化
- 自旋鎖
- 鎖消除
- 鎖粗化
- 多把鎖
- 活躍性
- 死鎖
- 形成
- 定位
- 活鎖
- 饑餓
- wait-ify
- 基本使用
- 代碼優(yōu)化
- park-un
- 安全分析
- 同步模式
- 保護(hù)性暫停
- 單任務(wù)版
- 多任務(wù)版
- 順序輸出
- 交替輸出
- 異步模式
- 傳統(tǒng)版
- 改進(jìn)版
- 阻塞隊(duì)列
- 內(nèi)存
- JMM
- 內(nèi)存模型
- 內(nèi)存交互
- 三大特性
- 可見性
- 原子性
- 有序性
- cache
- 緩存機(jī)制
- 緩存結(jié)構(gòu)
- 緩存使用
- 偽共享
- 緩存一致
- 處理機(jī)制
- volatile
- 同步機(jī)制
- 指令重排
- 底層原理
- 緩存一致
- 內(nèi)存屏障
- 交互規(guī)則
- 雙端檢鎖
- 檢鎖機(jī)制
- DCL問題
- 解決方法
- ha-be
- 設(shè)計(jì)模式
- 終止模式
- Balking
- 無鎖
- CAS
- 原理
- 樂觀鎖
- Atomic
- 常用API
- 原理分析
- 原子引用
- 原子數(shù)組
- 原子更新器
- 原子累加器
- Adder
- 優(yōu)化機(jī)制
- 偽共享
- 源碼解析
- ABA
- Unsafe
- final
- 原理
- 不可變
- State
- Local
- 基本介紹
- 基本使用
- 常用方法
- 應(yīng)用場(chǎng)景
- 實(shí)現(xiàn)原理
- 底層結(jié)構(gòu)
- 成員變量
- 成員方法
- LocalMap
- 成員屬性
- 成員方法
- 清理方法
- 內(nèi)存泄漏
- 變量傳遞
- 基本使用
- 實(shí)現(xiàn)原理
進(jìn)程
概述
進(jìn)程:程序是靜止的,進(jìn)程實(shí)體的運(yùn)行過程就是進(jìn)程,是系統(tǒng)進(jìn)行資源分配的基本單位
進(jìn)程的特征:并發(fā)性、異步性、動(dòng)態(tài)性、獨(dú)立性、結(jié)構(gòu)性
線程:線程是屬于進(jìn)程的,是一個(gè)基本的 CPU 執(zhí)行單元,是程序執(zhí)行流的最小單元。線程是進(jìn)程中的一個(gè)實(shí)體,是系統(tǒng)獨(dú)立調(diào)度的基本單位,線程本身不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源,與同屬一個(gè)進(jìn)程的其他線程共享進(jìn)程所擁有的全部資源
關(guān)系:一個(gè)進(jìn)程可以包含多個(gè)線程,這就是多線程,比如看視頻是進(jìn)程,圖畫、聲音、廣告等就是多個(gè)線程
線程的作用:使多道程序更好的并發(fā)執(zhí)行,提高資源利用率和系統(tǒng)吞吐量,增強(qiáng)操作系統(tǒng)的并發(fā)性能
并發(fā)并行:
- 并行:在同一時(shí)刻,有多個(gè)指令在多個(gè) CPU 上同時(shí)執(zhí)行
- 并發(fā):在同一時(shí)刻,有多個(gè)指令在單個(gè) CPU 上交替執(zhí)行
同步異步:
- 需要等待結(jié)果返回,才能繼續(xù)運(yùn)行就是同步
- 不需要等待結(jié)果返回,就能繼續(xù)運(yùn)行就是異步
參考視頻:https://www.bilibili.com/video/BV16J411h7Rd
筆記的整體結(jié)構(gòu)依據(jù)視頻編寫,并隨著學(xué)習(xí)的深入補(bǔ)充了很多知識(shí)
對(duì)比
線程進(jìn)程對(duì)比:
-
進(jìn)程基本上相互獨(dú)立的,而線程存在于進(jìn)程內(nèi),是進(jìn)程的一個(gè)子集
-
進(jìn)程擁有共享的資源,如內(nèi)存空間等,供其內(nèi)部的線程共享
-
進(jìn)程間通信較為復(fù)雜
同一臺(tái)計(jì)算機(jī)的進(jìn)程通信稱為 IPC(Inter-process communication)
- 信號(hào)量:信號(hào)量是一個(gè)計(jì)數(shù)器,用于多進(jìn)程對(duì)共享數(shù)據(jù)的訪問,解決同步相關(guān)的問題并避免競(jìng)爭(zhēng)條件
- 共享存儲(chǔ):多個(gè)進(jìn)程可以訪問同一塊內(nèi)存空間,需要使用信號(hào)量用來同步對(duì)共享存儲(chǔ)的訪問
- 管道通信:管道是用于連接一個(gè)讀進(jìn)程和一個(gè)寫進(jìn)程以實(shí)現(xiàn)它們之間通信的一個(gè)共享文件 pipe 文件,該文件同一時(shí)間只允許一個(gè)進(jìn)程訪問,所以只支持半雙工通信
- 匿名管道(Pipes):用于具有親緣關(guān)系的父子進(jìn)程間或者兄弟進(jìn)程之間的通信
- 命名管道(Names Pipes):以磁盤文件的方式存在,可以實(shí)現(xiàn)本機(jī)任意兩個(gè)進(jìn)程通信,遵循 FIFO
- 消息隊(duì)列:內(nèi)核中存儲(chǔ)消息的鏈表,由消息隊(duì)列標(biāo)識(shí)符標(biāo)識(shí),能在不同進(jìn)程之間提供全雙工通信,對(duì)比管道:
- 匿名管道存在于內(nèi)存中的文件;命名管道存在于實(shí)際的磁盤介質(zhì)或者文件系統(tǒng);消息隊(duì)列存放在內(nèi)核中,只有在內(nèi)核重啟(操作系統(tǒng)重啟)或者顯示地刪除一個(gè)消息隊(duì)列時(shí),該消息隊(duì)列才被真正刪除
- 讀進(jìn)程可以根據(jù)消息類型有選擇地接收消息,而不像 FIFO 那樣只能默認(rèn)地接收
不同計(jì)算機(jī)之間的進(jìn)程通信,需要通過網(wǎng)絡(luò),并遵守共同的協(xié)議,例如 HTTP
- 套接字:與其它通信機(jī)制不同的是,可用于不同機(jī)器間的互相通信
-
線程通信相對(duì)簡(jiǎn)單,因?yàn)榫€程之間共享進(jìn)程內(nèi)的內(nèi)存,一個(gè)例子是多個(gè)線程可以訪問同一個(gè)共享變量
Java 中的通信機(jī)制:volatile、等待/通知機(jī)制、join 方式、InheritableThreadLocal、MappedByteBuffer
-
線程更輕量,線程上下文切換成本一般上要比進(jìn)程上下文切換低
線程
創(chuàng)建線程
Thread
Thread 創(chuàng)建線程方式:創(chuàng)建線程類,匿名內(nèi)部類方式
- start() 方法底層其實(shí)是給 CPU 注冊(cè)當(dāng)前線程,并且觸發(fā) run() 方法執(zhí)行
- 線程的啟動(dòng)必須調(diào)用 start() 方法,如果線程直接調(diào)用 run() 方法,相當(dāng)于變成了普通類的執(zhí)行,此時(shí)主線程將只有執(zhí)行該線程
- 建議線程先創(chuàng)建子線程,主線程的任務(wù)放在之后,否則主線程(main)永遠(yuǎn)是先執(zhí)行完
Thread 構(gòu)造器:
public Thread()
public Thread(String name)
public class ThreadDemo {public static void main(String[] args) {Thread t = new MyThread();t.start();for(int i = 0 ; i < 100 ; i++ ){System.out.println("main線程" + i)}// main線程輸出放在上面 就變成有先后順序了,因?yàn)槭?main 線程驅(qū)動(dòng)的子線程運(yùn)行}
}
class MyThread extends Thread {@Overridepublic void run() {for(int i = 0 ; i < 100 ; i++ ) {System.out.println("子線程輸出:"+i)}}
}
繼承 Thread 類的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):編碼簡(jiǎn)單
- 缺點(diǎn):線程類已經(jīng)繼承了 Thread 類無法繼承其他類了,功能不能通過繼承拓展(單繼承的局限性)
Runnable
Runnable 創(chuàng)建線程方式:創(chuàng)建線程類,匿名內(nèi)部類方式
Thread 的構(gòu)造器:
public Thread(Runnable target)
public Thread(Runnable target, String name)
public class ThreadDemo {public static void main(String[] args) {Runnable target = new MyRunnable();Thread t1 = new Thread(target,"1號(hào)線程");t1.start();Thread t2 = new Thread(target);//Thread-0}
}public class MyRunnable implements Runnable{@Overridepublic void run() {for(int i = 0 ; i < 10 ; i++ ){System.out.println(Thread.currentThread().getName() + "->" + i);}}
}
Thread 類本身也是實(shí)現(xiàn)了 Runnable 接口,Thread 類中持有 Runnable 的屬性,執(zhí)行線程 run 方法底層是調(diào)用 Runnable#run:
public class Thread implements Runnable {private Runnable target;public void run() {if (target != null) {// 底層調(diào)用的是 Runnable 的 run 方法target.run();}}
}
Runnable 方式的優(yōu)缺點(diǎn):
-
缺點(diǎn):代碼復(fù)雜一點(diǎn)。
-
優(yōu)點(diǎn):
-
線程任務(wù)類只是實(shí)現(xiàn)了 Runnable 接口,可以繼續(xù)繼承其他類,避免了單繼承的局限性
-
同一個(gè)線程任務(wù)對(duì)象可以被包裝成多個(gè)線程對(duì)象
-
適合多個(gè)多個(gè)線程去共享同一個(gè)資源
-
實(shí)現(xiàn)解耦操作,線程任務(wù)代碼可以被多個(gè)線程共享,線程任務(wù)代碼和線程獨(dú)立
-
線程池可以放入實(shí)現(xiàn) Runnable 或 Callable 線程任務(wù)對(duì)象
-
?
Callable
實(shí)現(xiàn) Callable 接口:
- 定義一個(gè)線程任務(wù)類實(shí)現(xiàn) Callable 接口,申明線程執(zhí)行的結(jié)果類型
- 重寫線程任務(wù)類的 call 方法,這個(gè)方法可以直接返回執(zhí)行的結(jié)果
- 創(chuàng)建一個(gè) Callable 的線程任務(wù)對(duì)象
- 把 Callable 的線程任務(wù)對(duì)象包裝成一個(gè)未來任務(wù)對(duì)象
- 把未來任務(wù)對(duì)象包裝成線程對(duì)象
- 調(diào)用線程的 start() 方法啟動(dòng)線程
public FutureTask(Callable<V> callable)
:未來任務(wù)對(duì)象,在線程執(zhí)行完后得到線程的執(zhí)行結(jié)果
- FutureTask 就是 Runnable 對(duì)象,因?yàn)?Thread 類只能執(zhí)行 Runnable 實(shí)例的任務(wù)對(duì)象,所以把 Callable 包裝成未來任務(wù)對(duì)象
- 線程池部分詳解了 FutureTask 的源碼
public V get()
:同步等待 task 執(zhí)行完畢的結(jié)果,如果在線程中獲取另一個(gè)線程執(zhí)行結(jié)果,會(huì)阻塞等待,用于線程同步
- get() 線程會(huì)阻塞等待任務(wù)執(zhí)行完成
- run() 執(zhí)行完后會(huì)把結(jié)果設(shè)置到 FutureTask 的一個(gè)成員變量,get() 線程可以獲取到該變量的值
優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):同 Runnable,并且能得到線程執(zhí)行的結(jié)果
- 缺點(diǎn):編碼復(fù)雜
public class ThreadDemo {public static void main(String[] args) {Callable call = new MyCallable();FutureTask<String> task = new FutureTask<>(call);Thread t = new Thread(task);t.start();try {String s = task.get(); // 獲取call方法返回的結(jié)果(正常/異常結(jié)果)System.out.println(s);} catch (Exception e) {e.printStackTrace();}}public class MyCallable implements Callable<String> {@Override//重寫線程任務(wù)類方法public String call() throws Exception {return Thread.currentThread().getName() + "->" + "Hello World";}
}
線程方法
API
Thread 類 API:
方法 | 說明 |
---|---|
public void start() | 啟動(dòng)一個(gè)新線程,Java虛擬機(jī)調(diào)用此線程的 run 方法 |
public void run() | 線程啟動(dòng)后調(diào)用該方法 |
public void setName(String name) | 給當(dāng)前線程取名字 |
public void getName() | 獲取當(dāng)前線程的名字 線程存在默認(rèn)名稱:子線程是 Thread-索引,主線程是 main |
public static Thread currentThread() | 獲取當(dāng)前線程對(duì)象,代碼在哪個(gè)線程中執(zhí)行 |
public static void sleep(long time) | 讓當(dāng)前線程休眠多少毫秒再繼續(xù)執(zhí)行 Thread.sleep(0) : 讓操作系統(tǒng)立刻重新進(jìn)行一次 CPU 競(jìng)爭(zhēng) |
public static native void yield() | 提示線程調(diào)度器讓出當(dāng)前線程對(duì) CPU 的使用 |
public final int getPriority() | 返回此線程的優(yōu)先級(jí) |
public final void setPriority(int priority) | 更改此線程的優(yōu)先級(jí),常用 1 5 10 |
public void interrupt() | 中斷這個(gè)線程,異常處理機(jī)制 |
public static boolean interrupted() | 判斷當(dāng)前線程是否被打斷,清除打斷標(biāo)記 |
public boolean isInterrupted() | 判斷當(dāng)前線程是否被打斷,不清除打斷標(biāo)記 |
public final void join() | 等待這個(gè)線程結(jié)束 |
public final void join(long millis) | 等待這個(gè)線程死亡 millis 毫秒,0 意味著永遠(yuǎn)等待 |
public final native boolean isAlive() | 線程是否存活(還沒有運(yùn)行完畢) |
public final void setDaemon(boolean on) | 將此線程標(biāo)記為守護(hù)線程或用戶線程 |
run start
run:稱為線程體,包含了要執(zhí)行的這個(gè)線程的內(nèi)容,方法運(yùn)行結(jié)束,此線程隨即終止。直接調(diào)用 run 是在主線程中執(zhí)行了 run,沒有啟動(dòng)新的線程,需要順序執(zhí)行
start:使用 start 是啟動(dòng)新的線程,此線程處于就緒(可運(yùn)行)狀態(tài),通過新的線程間接執(zhí)行 run 中的代碼
說明:線程控制資源類
run() 方法中的異常不能拋出,只能 try/catch
- 因?yàn)楦割愔袥]有拋出任何異常,子類不能比父類拋出更多的異常
- 異常不能跨線程傳播回 main() 中,因此必須在本地進(jìn)行處理
sleep yield
sleep:
- 調(diào)用 sleep 會(huì)讓當(dāng)前線程從
Running
進(jìn)入Timed Waiting
狀態(tài)(阻塞) - sleep() 方法的過程中,線程不會(huì)釋放對(duì)象鎖
- 其它線程可以使用 interrupt 方法打斷正在睡眠的線程,這時(shí) sleep 方法會(huì)拋出 InterruptedException
- 睡眠結(jié)束后的線程未必會(huì)立刻得到執(zhí)行,需要搶占 CPU
- 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性
yield:
- 調(diào)用 yield 會(huì)讓提示線程調(diào)度器讓出當(dāng)前線程對(duì) CPU 的使用
- 具體的實(shí)現(xiàn)依賴于操作系統(tǒng)的任務(wù)調(diào)度器
- 會(huì)放棄 CPU 資源,鎖資源不會(huì)釋放
join
public final void join():等待這個(gè)線程結(jié)束
原理:調(diào)用者輪詢檢查線程 alive 狀態(tài),t1.join() 等價(jià)于:
public final synchronized void join(long millis) throws InterruptedException {// 調(diào)用者線程進(jìn)入 thread 的 waitSet 等待, 直到當(dāng)前線程運(yùn)行結(jié)束while (isAlive()) {wait(0);}
}
-
join 方法是被 synchronized 修飾的,本質(zhì)上是一個(gè)對(duì)象鎖,其內(nèi)部的 wait 方法調(diào)用也是釋放鎖的,但是釋放的是當(dāng)前的線程對(duì)象鎖,而不是外面的鎖
-
當(dāng)調(diào)用某個(gè)線程(t1)的 join 方法后,該線程(t1)搶占到 CPU 資源,就不再釋放,直到線程執(zhí)行完畢
線程同步:
- join 實(shí)現(xiàn)線程同步,因?yàn)闀?huì)阻塞等待另一個(gè)線程的結(jié)束,才能繼續(xù)向下運(yùn)行
- 需要外部共享變量,不符合面向?qū)ο蠓庋b的思想
- 必須等待線程結(jié)束,不能配合線程池使用
- Future 實(shí)現(xiàn)(同步):get() 方法阻塞等待執(zhí)行結(jié)果
- main 線程接收結(jié)果
- get 方法是讓調(diào)用線程同步等待
public class Test {static int r = 0;public static void main(String[] args) throws InterruptedException {test1();}private static void test1() throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}r = 10;});t1.start();t1.join();//不等待線程執(zhí)行結(jié)束,輸出的10System.out.println(r);}
}
interrupt
打斷線程
public void interrupt()
:打斷這個(gè)線程,異常處理機(jī)制
public static boolean interrupted()
:判斷當(dāng)前線程是否被打斷,打斷返回 true,清除打斷標(biāo)記,連續(xù)調(diào)用兩次一定返回 false
public boolean isInterrupted()
:判斷當(dāng)前線程是否被打斷,不清除打斷標(biāo)記
打斷的線程會(huì)發(fā)生上下文切換,操作系統(tǒng)會(huì)保存線程信息,搶占到 CPU 后會(huì)從中斷的地方接著運(yùn)行(打斷不是停止)
-
sleep、wait、join 方法都會(huì)讓線程進(jìn)入阻塞狀態(tài),打斷線程會(huì)清空打斷狀態(tài)(false)
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}, "t1");t1.start();Thread.sleep(500);t1.interrupt();System.out.println(" 打斷狀態(tài): {}" + t1.isInterrupted());// 打斷狀態(tài): {}false }
-
打斷正常運(yùn)行的線程:不會(huì)清空打斷狀態(tài)(true)
public static void main(String[] args) throws Exception {Thread t2 = new Thread(()->{while(true) {Thread current = Thread.currentThread();boolean interrupted = current.isInterrupted();if(interrupted) {System.out.println(" 打斷狀態(tài): {}" + interrupted);//打斷狀態(tài): {}truebreak;}}}, "t2");t2.start();Thread.sleep(500);t2.interrupt(); }
打斷 park
park 作用類似 sleep,打斷 park 線程,不會(huì)清空打斷狀態(tài)(true)
public static void main(String[] args) throws Exception {Thread t1 = new Thread(() -> {System.out.println("park...");LockSupport.park();System.out.println("unpark...");System.out.println("打斷狀態(tài):" + Thread.currentThread().isInterrupted());//打斷狀態(tài):true}, "t1");t1.start();Thread.sleep(2000);t1.interrupt();
}
如果打斷標(biāo)記已經(jīng)是 true, 則 park 會(huì)失效
LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不會(huì)阻塞
System.out.println("unpark...");//和上一個(gè)unpark同時(shí)執(zhí)行
可以修改獲取打斷狀態(tài)方法,使用 Thread.interrupted()
,清除打斷標(biāo)記
LockSupport 類在 同步 → park-un 詳解
終止模式
終止模式之兩階段終止模式:Two Phase Termination
目標(biāo):在一個(gè)線程 T1 中如何優(yōu)雅終止線程 T2?優(yōu)雅指的是給 T2 一個(gè)后置處理器
錯(cuò)誤思想:
- 使用線程對(duì)象的 stop() 方法停止線程:stop 方法會(huì)真正殺死線程,如果這時(shí)線程鎖住了共享資源,當(dāng)它被殺死后就再也沒有機(jī)會(huì)釋放鎖,其它線程將永遠(yuǎn)無法獲取鎖
- 使用 System.exit(int) 方法停止線程:目的僅是停止一個(gè)線程,但這種做法會(huì)讓整個(gè)程序都停止
兩階段終止模式圖示:

打斷線程可能在任何時(shí)間,所以需要考慮在任何時(shí)刻被打斷的處理方法:
public class Test {public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt = new TwoPhaseTermination();tpt.start();Thread.sleep(3500);tpt.stop();}
}
class TwoPhaseTermination {private Thread monitor;// 啟動(dòng)監(jiān)控線程public void start() {monitor = new Thread(new Runnable() {@Overridepublic void run() {while (true) {Thread thread = Thread.currentThread();if (thread.isInterrupted()) {System.out.println("后置處理");break;}try {Thread.sleep(1000); // 睡眠System.out.println("執(zhí)行監(jiān)控記錄"); // 在此被打斷不會(huì)異常} catch (InterruptedException e) { // 在睡眠期間被打斷,進(jìn)入異常處理的邏輯e.printStackTrace();// 重新設(shè)置打斷標(biāo)記,打斷 sleep 會(huì)清除打斷狀態(tài)thread.interrupt();}}}});monitor.start();}// 停止監(jiān)控線程public void stop() {monitor.interrupt();}
}
daemon
public final void setDaemon(boolean on)
:如果是 true ,將此線程標(biāo)記為守護(hù)線程
線程啟動(dòng)前調(diào)用此方法:
Thread t = new Thread() {@Overridepublic void run() {System.out.println("running");}
};
// 設(shè)置該線程為守護(hù)線程
t.setDaemon(true);
t.start();
用戶線程:平常創(chuàng)建的普通線程
守護(hù)線程:服務(wù)于用戶線程,只要其它非守護(hù)線程運(yùn)行結(jié)束了,即使守護(hù)線程代碼沒有執(zhí)行完,也會(huì)強(qiáng)制結(jié)束。守護(hù)進(jìn)程是脫離于終端并且在后臺(tái)運(yùn)行的進(jìn)程,脫離終端是為了避免在執(zhí)行的過程中的信息在終端上顯示
說明:當(dāng)運(yùn)行的線程都是守護(hù)線程,Java 虛擬機(jī)將退出,因?yàn)槠胀ň€程執(zhí)行完后,JVM 是守護(hù)線程,不會(huì)繼續(xù)運(yùn)行下去
常見的守護(hù)線程:
- 垃圾回收器線程就是一種守護(hù)線程
- Tomcat 中的 Acceptor 和 Poller 線程都是守護(hù)線程,所以 Tomcat 接收到 shutdown 命令后,不會(huì)等待它們處理完當(dāng)前請(qǐng)求
不推薦
不推薦使用的方法,這些方法已過時(shí),容易破壞同步代碼塊,造成線程死鎖:
-
public final void stop()
:停止線程運(yùn)行廢棄原因:方法粗暴,除非可能執(zhí)行 finally 代碼塊以及釋放 synchronized 外,線程將直接被終止,如果線程持有 JUC 的互斥鎖可能導(dǎo)致鎖來不及釋放,造成其他線程永遠(yuǎn)等待的局面
-
public final void suspend()
:掛起(暫停)線程運(yùn)行廢棄原因:如果目標(biāo)線程在暫停時(shí)對(duì)系統(tǒng)資源持有鎖,則在目標(biāo)線程恢復(fù)之前沒有線程可以訪問該資源,如果恢復(fù)目標(biāo)線程的線程在調(diào)用 resume 之前會(huì)嘗試訪問此共享資源,則會(huì)導(dǎo)致死鎖
-
public final void resume()
:恢復(fù)線程運(yùn)行
線程原理
運(yùn)行機(jī)制
Java Virtual Machine Stacks(Java 虛擬機(jī)棧):每個(gè)線程啟動(dòng)后,虛擬機(jī)就會(huì)為其分配一塊棧內(nèi)存
- 每個(gè)棧由多個(gè)棧幀(Frame)組成,對(duì)應(yīng)著每次方法調(diào)用時(shí)所占用的內(nèi)存
- 每個(gè)線程只能有一個(gè)活動(dòng)棧幀,對(duì)應(yīng)著當(dāng)前正在執(zhí)行的那個(gè)方法
線程上下文切換(Thread Context Switch):一些原因?qū)е?CPU 不再執(zhí)行當(dāng)前線程,轉(zhuǎn)而執(zhí)行另一個(gè)線程
- 線程的 CPU 時(shí)間片用完
- 垃圾回收
- 有更高優(yōu)先級(jí)的線程需要運(yùn)行
- 線程自己調(diào)用了 sleep、yield、wait、join、park 等方法
程序計(jì)數(shù)器(Program Counter Register):記住下一條 JVM 指令的執(zhí)行地址,是線程私有的
當(dāng) Context Switch 發(fā)生時(shí),需要由操作系統(tǒng)保存當(dāng)前線程的狀態(tài)(PCB 中),并恢復(fù)另一個(gè)線程的狀態(tài),包括程序計(jì)數(shù)器、虛擬機(jī)棧中每個(gè)棧幀的信息,如局部變量、操作數(shù)棧、返回地址等
JVM 規(guī)范并沒有限定線程模型,以 HotSopot 為例:
- Java 的線程是內(nèi)核級(jí)線程(1:1 線程模型),每個(gè) Java 線程都映射到一個(gè)操作系統(tǒng)原生線程,需要消耗一定的內(nèi)核資源(堆棧)
- 線程的調(diào)度是在內(nèi)核態(tài)運(yùn)行的,而線程中的代碼是在用戶態(tài)運(yùn)行,所以線程切換(狀態(tài)改變)會(huì)導(dǎo)致用戶與內(nèi)核態(tài)轉(zhuǎn)換進(jìn)行系統(tǒng)調(diào)用,這是非常消耗性能
Java 中 main 方法啟動(dòng)的是一個(gè)進(jìn)程也是一個(gè)主線程,main 方法里面的其他線程均為子線程,main 線程是這些線程的父線程
線程調(diào)度
線程調(diào)度指系統(tǒng)為線程分配處理器使用權(quán)的過程,方式有兩種:協(xié)同式線程調(diào)度、搶占式線程調(diào)度(Java 選擇)
協(xié)同式線程調(diào)度:線程的執(zhí)行時(shí)間由線程本身控制
- 優(yōu)點(diǎn):線程做完任務(wù)才通知系統(tǒng)切換到其他線程,相當(dāng)于所有線程串行執(zhí)行,不會(huì)出現(xiàn)線程同步問題
- 缺點(diǎn):線程執(zhí)行時(shí)間不可控,如果代碼編寫出現(xiàn)問題,可能導(dǎo)致程序一直阻塞,引起系統(tǒng)的奔潰
搶占式線程調(diào)度:線程的執(zhí)行時(shí)間由系統(tǒng)分配
- 優(yōu)點(diǎn):線程執(zhí)行時(shí)間可控,不會(huì)因?yàn)橐粋€(gè)線程的問題而導(dǎo)致整體系統(tǒng)不可用
- 缺點(diǎn):無法主動(dòng)為某個(gè)線程多分配時(shí)間
Java 提供了線程優(yōu)先級(jí)的機(jī)制,優(yōu)先級(jí)會(huì)提示(hint)調(diào)度器優(yōu)先調(diào)度該線程,但這僅僅是一個(gè)提示,調(diào)度器可以忽略它。在線程的就緒狀態(tài)時(shí),如果 CPU 比較忙,那么優(yōu)先級(jí)高的線程會(huì)獲得更多的時(shí)間片,但 CPU 閑時(shí),優(yōu)先級(jí)幾乎沒作用
說明:并不能通過優(yōu)先級(jí)來判斷線程執(zhí)行的先后順序
未來優(yōu)化
內(nèi)核級(jí)線程調(diào)度的成本較大,所以引入了更輕量級(jí)的協(xié)程。用戶線程的調(diào)度由用戶自己實(shí)現(xiàn)(多對(duì)一的線程模型,多個(gè)用戶線程映射到一個(gè)內(nèi)核級(jí)線程),被設(shè)計(jì)為協(xié)同式調(diào)度,所以叫協(xié)程
- 有棧協(xié)程:協(xié)程會(huì)完整的做調(diào)用棧的保護(hù)、恢復(fù)工作,所以叫有棧協(xié)程
- 無棧協(xié)程:本質(zhì)上是一種有限狀態(tài)機(jī),狀態(tài)保存在閉包里,比有棧協(xié)程更輕量,但是功能有限
有棧協(xié)程中有一種特例叫纖程,在新并發(fā)模型中,一段纖程的代碼被分為兩部分,執(zhí)行過程和調(diào)度器:
- 執(zhí)行過程:用于維護(hù)執(zhí)行現(xiàn)場(chǎng),保護(hù)、恢復(fù)上下文狀態(tài)
- 調(diào)度器:負(fù)責(zé)編排所有要執(zhí)行的代碼順序
線程狀態(tài)
進(jìn)程的狀態(tài)參考操作系統(tǒng):創(chuàng)建態(tài)、就緒態(tài)、運(yùn)行態(tài)、阻塞態(tài)、終止態(tài)
線程由生到死的完整過程(生命周期):當(dāng)線程被創(chuàng)建并啟動(dòng)以后,既不是一啟動(dòng)就進(jìn)入了執(zhí)行狀態(tài),也不是一直處于執(zhí)行狀態(tài),在 API 中 java.lang.Thread.State
這個(gè)枚舉中給出了六種線程狀態(tài):
線程狀態(tài) | 導(dǎo)致狀態(tài)發(fā)生條件 |
---|---|
NEW(新建) | 線程剛被創(chuàng)建,但是并未啟動(dòng),還沒調(diào)用 start 方法,只有線程對(duì)象,沒有線程特征 |
Runnable(可運(yùn)行) | 線程可以在 Java 虛擬機(jī)中運(yùn)行的狀態(tài),可能正在運(yùn)行自己代碼,也可能沒有,這取決于操作系統(tǒng)處理器,調(diào)用了 t.start() 方法:就緒(經(jīng)典叫法) |
Blocked(阻塞) | 當(dāng)一個(gè)線程試圖獲取一個(gè)對(duì)象鎖,而該對(duì)象鎖被其他的線程持有,則該線程進(jìn)入 Blocked 狀態(tài);當(dāng)該線程持有鎖時(shí),該線程將變成 Runnable 狀態(tài) |
Waiting(無限等待) | 一個(gè)線程在等待另一個(gè)線程執(zhí)行一個(gè)(喚醒)動(dòng)作時(shí),該線程進(jìn)入 Waiting 狀態(tài),進(jìn)入這個(gè)狀態(tài)后不能自動(dòng)喚醒,必須等待另一個(gè)線程調(diào)用 notify 或者 notifyAll 方法才能喚醒 |
Timed Waiting (限期等待) | 有幾個(gè)方法有超時(shí)參數(shù),調(diào)用將進(jìn)入 Timed Waiting 狀態(tài),這一狀態(tài)將一直保持到超時(shí)期滿或者接收到喚醒通知。帶有超時(shí)參數(shù)的常用方法有 Thread.sleep 、Object.wait |
Teminated(結(jié)束) | run 方法正常退出而死亡,或者因?yàn)闆]有捕獲的異常終止了 run 方法而死亡 |
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ISw9ZWBy-1678148961772)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-線程6種狀態(tài).png)]
-
NEW → RUNNABLE:當(dāng)調(diào)用 t.start() 方法時(shí),由 NEW → RUNNABLE
-
RUNNABLE <–> WAITING:
-
調(diào)用 obj.wait() 方法時(shí)
調(diào)用 obj.notify()、obj.notifyAll()、t.interrupt():
- 競(jìng)爭(zhēng)鎖成功,t 線程從 WAITING → RUNNABLE
- 競(jìng)爭(zhēng)鎖失敗,t 線程從 WAITING → BLOCKED
-
當(dāng)前線程調(diào)用 t.join() 方法,注意是當(dāng)前線程在 t 線程對(duì)象的監(jiān)視器上等待
-
當(dāng)前線程調(diào)用 LockSupport.park() 方法
-
-
RUNNABLE <–> TIMED_WAITING:調(diào)用 obj.wait(long n) 方法、當(dāng)前線程調(diào)用 t.join(long n) 方法、當(dāng)前線程調(diào)用 Thread.sleep(long n)
-
RUNNABLE <–> BLOCKED:t 線程用 synchronized(obj) 獲取了對(duì)象鎖時(shí)競(jìng)爭(zhēng)失敗
查看線程
Windows:
- 任務(wù)管理器可以查看進(jìn)程和線程數(shù),也可以用來殺死進(jìn)程
- tasklist 查看進(jìn)程
- taskkill 殺死進(jìn)程
Linux:
- ps -ef 查看所有進(jìn)程
- ps -fT -p 查看某個(gè)進(jìn)程(PID)的所有線程
- kill 殺死進(jìn)程
- top 按大寫 H 切換是否顯示線程
- top -H -p 查看某個(gè)進(jìn)程(PID)的所有線程
Java:
- jps 命令查看所有 Java 進(jìn)程
- jstack 查看某個(gè) Java 進(jìn)程(PID)的所有線程狀態(tài)
- jconsole 來查看某個(gè) Java 進(jìn)程中線程的運(yùn)行情況(圖形界面)
同步
臨界區(qū)
臨界資源:一次僅允許一個(gè)進(jìn)程使用的資源成為臨界資源
臨界區(qū):訪問臨界資源的代碼塊
競(jìng)態(tài)條件:多個(gè)線程在臨界區(qū)內(nèi)執(zhí)行,由于代碼的執(zhí)行序列不同而導(dǎo)致結(jié)果無法預(yù)測(cè),稱之為發(fā)生了競(jìng)態(tài)條件
一個(gè)程序運(yùn)行多個(gè)線程是沒有問題,多個(gè)線程讀共享資源也沒有問題,在多個(gè)線程對(duì)共享資源讀寫操作時(shí)發(fā)生指令交錯(cuò),就會(huì)出現(xiàn)問題
為了避免臨界區(qū)的競(jìng)態(tài)條件發(fā)生(解決線程安全問題):
- 阻塞式的解決方案:synchronized,lock
- 非阻塞式的解決方案:原子變量
管程(monitor):由局部于自己的若干公共變量和所有訪問這些公共變量的過程所組成的軟件模塊,保證同一時(shí)刻只有一個(gè)進(jìn)程在管程內(nèi)活動(dòng),即管程內(nèi)定義的操作在同一時(shí)刻只被一個(gè)進(jìn)程調(diào)用(由編譯器實(shí)現(xiàn))
synchronized:對(duì)象鎖,保證了臨界區(qū)內(nèi)代碼的原子性,采用互斥的方式讓同一時(shí)刻至多只有一個(gè)線程能持有對(duì)象鎖,其它線程獲取這個(gè)對(duì)象鎖時(shí)會(huì)阻塞,保證擁有鎖的線程可以安全的執(zhí)行臨界區(qū)內(nèi)的代碼,不用擔(dān)心線程上下文切換
互斥和同步都可以采用 synchronized 關(guān)鍵字來完成,區(qū)別:
- 互斥是保證臨界區(qū)的競(jìng)態(tài)條件發(fā)生,同一時(shí)刻只能有一個(gè)線程執(zhí)行臨界區(qū)代碼
- 同步是由于線程執(zhí)行的先后、順序不同、需要一個(gè)線程等待其它線程運(yùn)行到某個(gè)點(diǎn)
性能:
- 線程安全,性能差
- 線程不安全性能好,假如開發(fā)中不會(huì)存在多線程安全問題,建議使用線程不安全的設(shè)計(jì)類
syn-ed
使用鎖
同步塊
鎖對(duì)象:理論上可以是任意的唯一對(duì)象
synchronized 是可重入、不公平的重量級(jí)鎖
原則上:
- 鎖對(duì)象建議使用共享資源
- 在實(shí)例方法中使用 this 作為鎖對(duì)象,鎖住的 this 正好是共享資源
- 在靜態(tài)方法中使用類名 .class 字節(jié)碼作為鎖對(duì)象,因?yàn)殪o態(tài)成員屬于類,被所有實(shí)例對(duì)象共享,所以需要鎖住類
同步代碼塊格式:
synchronized(鎖對(duì)象){// 訪問共享資源的核心代碼
}
實(shí)例:
public class demo {static int counter = 0;//static修飾,則元素是屬于類本身的,不屬于對(duì)象 ,與類一起加載一次,只有一個(gè)static final Object room = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();System.out.println(counter);}
}
同步方法
把出現(xiàn)線程安全問題的核心方法鎖起來,每次只能一個(gè)線程進(jìn)入訪問
synchronized 修飾的方法的不具備繼承性,所以子類是線程不安全的,如果子類的方法也被 synchronized 修飾,兩個(gè)鎖對(duì)象其實(shí)是一把鎖,而且是子類對(duì)象作為鎖
用法:直接給方法加上一個(gè)修飾符 synchronized
//同步方法
修飾符 synchronized 返回值類型 方法名(方法參數(shù)) { 方法體;
}
//同步靜態(tài)方法
修飾符 static synchronized 返回值類型 方法名(方法參數(shù)) { 方法體;
}
同步方法底層也是有鎖對(duì)象的:
-
如果方法是實(shí)例方法:同步方法默認(rèn)用 this 作為的鎖對(duì)象
public synchronized void test() {} //等價(jià)于 public void test() {synchronized(this) {} }
-
如果方法是靜態(tài)方法:同步方法默認(rèn)用類名 .class 作為的鎖對(duì)象
class Test{public synchronized static void test() {} } //等價(jià)于 class Test{public void test() {synchronized(Test.class) {}} }
線程八鎖
線程八鎖就是考察 synchronized 鎖住的是哪個(gè)對(duì)象,直接百度搜索相關(guān)的實(shí)例
說明:主要關(guān)注鎖住的對(duì)象是不是同一個(gè)
- 鎖住類對(duì)象,所有類的實(shí)例的方法都是安全的,類的所有實(shí)例都相當(dāng)于同一把鎖
- 鎖住 this 對(duì)象,只有在當(dāng)前實(shí)例對(duì)象的線程內(nèi)是安全的,如果有多個(gè)實(shí)例就不安全
線程不安全:因?yàn)殒i住的不是同一個(gè)對(duì)象,線程 1 調(diào)用 a 方法鎖住的類對(duì)象,線程 2 調(diào)用 b 方法鎖住的 n2 對(duì)象,不是同一個(gè)對(duì)象
class Number{public static synchronized void a(){Thread.sleep(1000);System.out.println("1");}public synchronized void b() {System.out.println("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
線程安全:因?yàn)?n1 調(diào)用 a() 方法,鎖住的是類對(duì)象,n2 調(diào)用 b() 方法,鎖住的也是類對(duì)象,所以線程安全
class Number{public static synchronized void a(){Thread.sleep(1000);System.out.println("1");}public static synchronized void b() {System.out.println("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
鎖原理
Monitor
Monitor 被翻譯為監(jiān)視器或管程
每個(gè) Java 對(duì)象都可以關(guān)聯(lián)一個(gè) Monitor 對(duì)象,Monitor 也是 class,其實(shí)例存儲(chǔ)在堆中,如果使用 synchronized 給對(duì)象上鎖(重量級(jí))之后,該對(duì)象頭的 Mark Word 中就被設(shè)置指向 Monitor 對(duì)象的指針,這就是重量級(jí)鎖
-
Mark Word 結(jié)構(gòu):最后兩位是鎖標(biāo)志位
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-k8efRwl1-1678148961773)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord結(jié)構(gòu)32位.png)]
-
64 位虛擬機(jī) Mark Word:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-AHDYaW0y-1678148961773)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord結(jié)構(gòu)64位.png)]
工作流程:
- 開始時(shí) Monitor 中 Owner 為 null
- 當(dāng) Thread-2 執(zhí)行 synchronized(obj) 就會(huì)將 Monitor 的所有者 Owner 置為 Thread-2,Monitor 中只能有一個(gè) Owner,obj 對(duì)象的 Mark Word 指向 Monitor,把對(duì)象原有的 MarkWord 存入線程棧中的鎖記錄中(輕量級(jí)鎖部分詳解)
- 在 Thread-2 上鎖的過程,Thread-3、Thread-4、Thread-5 也執(zhí)行 synchronized(obj),就會(huì)進(jìn)入 EntryList BLOCKED(雙向鏈表)
- Thread-2 執(zhí)行完同步代碼塊的內(nèi)容,根據(jù) obj 對(duì)象頭中 Monitor 地址尋找,設(shè)置 Owner 為空,把線程棧的鎖記錄中的對(duì)象頭的值設(shè)置回 MarkWord
- 喚醒 EntryList 中等待的線程來競(jìng)爭(zhēng)鎖,競(jìng)爭(zhēng)是非公平的,如果這時(shí)有新的線程想要獲取鎖,可能直接就搶占到了,阻塞隊(duì)列的線程就會(huì)繼續(xù)阻塞
- WaitSet 中的 Thread-0,是以前獲得過鎖,但條件不滿足進(jìn)入 WAITING 狀態(tài)的線程(wait-notify 機(jī)制)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-QlyNLJGc-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png)]
注意:
- synchronized 必須是進(jìn)入同一個(gè)對(duì)象的 Monitor 才有上述的效果
- 不加 synchronized 的對(duì)象不會(huì)關(guān)聯(lián)監(jiān)視器,不遵從以上規(guī)則
字節(jié)碼
代碼:
public static void main(String[] args) {Object lock = new Object();synchronized (lock) {System.out.println("ok");}
}
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V,非虛方法
7: astore_1 // lock引用 -> lock
8: aload_1 // lock (synchronized開始)
9: dup // 一份用來初始化,一份用來引用
10: astore_2 // lock引用 -> slot 2
11: monitorenter // 【將 lock對(duì)象 MarkWord 置為 Monitor 指針】
12: getstatic #3 // System.out
15: ldc #4 // "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
20: aload_2 // slot 2(lock引用)
21: monitorexit // 【將 lock對(duì)象 MarkWord 重置, 喚醒 EntryList】
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // slot 2(lock引用)
27: monitorexit // 【將 lock對(duì)象 MarkWord 重置, 喚醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:from to target type12 22 25 any25 28 25 any
LineNumberTable: ...
LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 lock Ljava/lang/Object;
說明:
- 通過異常 try-catch 機(jī)制,確保一定會(huì)被解鎖
- 方法級(jí)別的 synchronized 不會(huì)在字節(jié)碼指令中有所體現(xiàn)
鎖升級(jí)
升級(jí)過程
synchronized 是可重入、不公平的重量級(jí)鎖,所以可以對(duì)其進(jìn)行優(yōu)化
無鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖 // 隨著競(jìng)爭(zhēng)的增加,只能鎖升級(jí),不能降級(jí)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-wxuReiq4-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-鎖升級(jí)過程.png)]
偏向鎖
偏向鎖的思想是偏向于讓第一個(gè)獲取鎖對(duì)象的線程,這個(gè)線程之后重新獲取該鎖不再需要同步操作:
-
當(dāng)鎖對(duì)象第一次被線程獲得的時(shí)候進(jìn)入偏向狀態(tài),標(biāo)記為 101,同時(shí)使用 CAS 操作將線程 ID 記錄到 Mark Word。如果 CAS 操作成功,這個(gè)線程以后進(jìn)入這個(gè)鎖相關(guān)的同步塊,查看這個(gè)線程 ID 是自己的就表示沒有競(jìng)爭(zhēng),就不需要再進(jìn)行任何同步操作
-
當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖對(duì)象時(shí),偏向狀態(tài)就宣告結(jié)束,此時(shí)撤銷偏向(Revoke Bias)后恢復(fù)到未鎖定或輕量級(jí)鎖狀態(tài)
構(gòu)64位.png)
一個(gè)對(duì)象創(chuàng)建時(shí):
-
如果開啟了偏向鎖(默認(rèn)開啟),那么對(duì)象創(chuàng)建后,MarkWord 值為 0x05 即最后 3 位為 101,thread、epoch、age 都為 0
-
偏向鎖是默認(rèn)是延遲的,不會(huì)在程序啟動(dòng)時(shí)立即生效,如果想避免延遲,可以加 VM 參數(shù)
-XX:BiasedLockingStartupDelay=0
來禁用延遲。JDK 8 延遲 4s 開啟偏向鎖原因:在剛開始執(zhí)行代碼時(shí),會(huì)有好多線程來搶鎖,如果開偏向鎖效率反而降低 -
當(dāng)一個(gè)對(duì)象已經(jīng)計(jì)算過 hashCode,就再也無法進(jìn)入偏向狀態(tài)了
-
添加 VM 參數(shù)
-XX:-UseBiasedLocking
禁用偏向鎖
撤銷偏向鎖的狀態(tài):
- 調(diào)用對(duì)象的 hashCode:偏向鎖的對(duì)象 MarkWord 中存儲(chǔ)的是線程 id,調(diào)用 hashCode 導(dǎo)致偏向鎖被撤銷
- 當(dāng)有其它線程使用偏向鎖對(duì)象時(shí),會(huì)將偏向鎖升級(jí)為輕量級(jí)鎖
- 調(diào)用 wait/notify,需要申請(qǐng) Monitor,進(jìn)入 WaitSet
批量撤銷:如果對(duì)象被多個(gè)線程訪問,但沒有競(jìng)爭(zhēng),這時(shí)偏向了線程 T1 的對(duì)象仍有機(jī)會(huì)重新偏向 T2,重偏向會(huì)重置對(duì)象的 Thread ID
-
批量重偏向:當(dāng)撤銷偏向鎖閾值超過 20 次后,JVM 會(huì)覺得是不是偏向錯(cuò)了,于是在給這些對(duì)象加鎖時(shí)重新偏向至加鎖線程
-
批量撤銷:當(dāng)撤銷偏向鎖閾值超過 40 次后,JVM 會(huì)覺得自己確實(shí)偏向錯(cuò)了,根本就不該偏向,于是整個(gè)類的所有對(duì)象都會(huì)變?yōu)椴豢善虻?#xff0c;新建的對(duì)象也是不可偏向的
輕量級(jí)鎖
一個(gè)對(duì)象有多個(gè)線程要加鎖,但加鎖的時(shí)間是錯(cuò)開的(沒有競(jìng)爭(zhēng)),可以使用輕量級(jí)鎖來優(yōu)化,輕量級(jí)鎖對(duì)使用者是透明的(不可見)
可重入鎖:線程可以進(jìn)入任何一個(gè)它已經(jīng)擁有的鎖所同步著的代碼塊,可重入鎖最大的作用是避免死鎖
輕量級(jí)鎖在沒有競(jìng)爭(zhēng)時(shí)(鎖重入時(shí)),每次重入仍然需要執(zhí)行 CAS 操作,Java 6 才引入的偏向鎖來優(yōu)化
鎖重入實(shí)例:
static final Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步塊 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步塊 B}
}
-
創(chuàng)建鎖記錄(Lock Record)對(duì)象,每個(gè)線程的棧幀都會(huì)包含一個(gè)鎖記錄的結(jié)構(gòu),存儲(chǔ)鎖定對(duì)象的 Mark Word
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-6jJtP7Tf-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-輕量級(jí)鎖原理1.png)]
-
讓鎖記錄中 Object reference 指向鎖住的對(duì)象,并嘗試用 CAS 替換 Object 的 Mark Word,將 Mark Word 的值存入鎖記錄
-
如果 CAS 替換成功,對(duì)象頭中存儲(chǔ)了鎖記錄地址和狀態(tài) 00(輕量級(jí)鎖) ,表示由該線程給對(duì)象加鎖
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-la2KBNpp-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-輕量級(jí)鎖原理2.png)] -
如果 CAS 失敗,有兩種情況:
- 如果是其它線程已經(jīng)持有了該 Object 的輕量級(jí)鎖,這時(shí)表明有競(jìng)爭(zhēng),進(jìn)入鎖膨脹過程
- 如果是線程自己執(zhí)行了 synchronized 鎖重入,就添加一條 Lock Record 作為重入的計(jì)數(shù)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-DASt1Fz6-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-輕量級(jí)鎖原理3.png)]
-
當(dāng)退出 synchronized 代碼塊(解鎖時(shí))
- 如果有取值為 null 的鎖記錄,表示有重入,這時(shí)重置鎖記錄,表示重入計(jì)數(shù)減 1
- 如果鎖記錄的值不為 null,這時(shí)使用 CAS 將 Mark Word 的值恢復(fù)給對(duì)象頭
- 成功,則解鎖成功
- 失敗,說明輕量級(jí)鎖進(jìn)行了鎖膨脹或已經(jīng)升級(jí)為重量級(jí)鎖,進(jìn)入重量級(jí)鎖解鎖流程
鎖膨脹
在嘗試加輕量級(jí)鎖的過程中,CAS 操作無法成功,可能是其它線程為此對(duì)象加上了輕量級(jí)鎖(有競(jìng)爭(zhēng)),這時(shí)需要進(jìn)行鎖膨脹,將輕量級(jí)鎖變?yōu)?strong>重量級(jí)鎖
-
當(dāng) Thread-1 進(jìn)行輕量級(jí)加鎖時(shí),Thread-0 已經(jīng)對(duì)該對(duì)象加了輕量級(jí)鎖
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-dxq4Ugn6-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量級(jí)鎖原理1.png)]
-
Thread-1 加輕量級(jí)鎖失敗,進(jìn)入鎖膨脹流程:為 Object 對(duì)象申請(qǐng) Monitor 鎖,通過 Object 對(duì)象頭獲取到持鎖線程,將 Monitor 的 Owner 置為 Thread-0,將 Object 的對(duì)象頭指向重量級(jí)鎖地址,然后自己進(jìn)入 Monitor 的 EntryList BLOCKED
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-iIcMjg4a-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量級(jí)鎖原理2.png)]
-
當(dāng) Thread-0 退出同步塊解鎖時(shí),使用 CAS 將 Mark Word 的值恢復(fù)給對(duì)象頭失敗,這時(shí)進(jìn)入重量級(jí)解鎖流程,即按照 Monitor 地址找到 Monitor 對(duì)象,設(shè)置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線程
鎖優(yōu)化
自旋鎖
重量級(jí)鎖競(jìng)爭(zhēng)時(shí),嘗試獲取鎖的線程不會(huì)立即阻塞,可以使用自旋(默認(rèn) 10 次)來進(jìn)行優(yōu)化,采用循環(huán)的方式去嘗試獲取鎖
注意:
- 自旋占用 CPU 時(shí)間,單核 CPU 自旋就是浪費(fèi)時(shí)間,因?yàn)橥粫r(shí)刻只能運(yùn)行一個(gè)線程,多核 CPU 自旋才能發(fā)揮優(yōu)勢(shì)
- 自旋失敗的線程會(huì)進(jìn)入阻塞狀態(tài)
優(yōu)點(diǎn):不會(huì)進(jìn)入阻塞狀態(tài),減少線程上下文切換的消耗
缺點(diǎn):當(dāng)自旋的線程越來越多時(shí),會(huì)不斷的消耗 CPU 資源
自旋鎖情況:
-
自旋成功的情況:
-
自旋失敗的情況:
自旋鎖說明:
- 在 Java 6 之后自旋鎖是自適應(yīng)的,比如對(duì)象剛剛的一次自旋操作成功過,那么認(rèn)為這次自旋成功的可能性會(huì)高,就多自旋幾次;反之,就少自旋甚至不自旋,比較智能
- Java 7 之后不能控制是否開啟自旋功能,由 JVM 控制
//手寫自旋鎖
public class SpinLock {// 泛型裝的是Thread,原子引用線程AtomicReference<Thread> atomicReference = new AtomicReference<>();public void lock() {Thread thread = Thread.currentThread();System.out.println(thread.getName() + " come in");//開始自旋,期望值為null,更新值是當(dāng)前線程while (!atomicReference.compareAndSet(null, thread)) {Thread.sleep(1000);System.out.println(thread.getName() + " 正在自旋");}System.out.println(thread.getName() + " 自旋成功");}public void unlock() {Thread thread = Thread.currentThread();//線程使用完鎖把引用變?yōu)閚ullatomicReference.compareAndSet(thread, null);System.out.println(thread.getName() + " invoke unlock");}public static void main(String[] args) throws InterruptedException {SpinLock lock = new SpinLock();new Thread(() -> {//占有鎖lock.lock();Thread.sleep(10000); //釋放鎖lock.unlock();},"t1").start();// 讓main線程暫停1秒,使得t1線程,先執(zhí)行Thread.sleep(1000);new Thread(() -> {lock.lock();lock.unlock();},"t2").start();}
}
鎖消除
鎖消除是指對(duì)于被檢測(cè)出不可能存在競(jìng)爭(zhēng)的共享數(shù)據(jù)的鎖進(jìn)行消除,這是 JVM 即時(shí)編譯器的優(yōu)化
鎖消除主要是通過逃逸分析來支持,如果堆上的共享數(shù)據(jù)不可能逃逸出去被其它線程訪問到,那么就可以把它們當(dāng)成私有數(shù)據(jù)對(duì)待,也就可以將它們的鎖進(jìn)行消除(同步消除:JVM 逃逸分析)
鎖粗化
對(duì)相同對(duì)象多次加鎖,導(dǎo)致線程發(fā)生多次重入,頻繁的加鎖操作就會(huì)導(dǎo)致性能損耗,可以使用鎖粗化方式優(yōu)化
如果虛擬機(jī)探測(cè)到一串的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部
-
一些看起來沒有加鎖的代碼,其實(shí)隱式的加了很多鎖:
public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3; }
-
String 是一個(gè)不可變的類,編譯器會(huì)對(duì) String 的拼接自動(dòng)優(yōu)化。在 JDK 1.5 之前,轉(zhuǎn)化為 StringBuffer 對(duì)象的連續(xù) append() 操作,每個(gè) append() 方法中都有一個(gè)同步塊
public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString(); }
擴(kuò)展到第一個(gè) append() 操作之前直至最后一個(gè) append() 操作之后,只需要加鎖一次就可以
多把鎖
多把不相干的鎖:一間大屋子有兩個(gè)功能睡覺、學(xué)習(xí),互不相干。現(xiàn)在一人要學(xué)習(xí),一人要睡覺,如果只用一間屋子(一個(gè)對(duì)象鎖)的話,那么并發(fā)度很低
將鎖的粒度細(xì)分:
- 好處,是可以增強(qiáng)并發(fā)度
- 壞處,如果一個(gè)線程需要同時(shí)獲得多把鎖,就容易發(fā)生死鎖
解決方法:準(zhǔn)備多個(gè)對(duì)象鎖
public static void main(String[] args) {BigRoom bigRoom = new BigRoom();new Thread(() -> { bigRoom.study(); }).start();new Thread(() -> { bigRoom.sleep(); }).start();
}
class BigRoom {private final Object studyRoom = new Object();private final Object sleepRoom = new Object();public void sleep() throws InterruptedException {synchronized (sleepRoom) {System.out.println("sleeping 2 小時(shí)");Thread.sleep(2000);}}public void study() throws InterruptedException {synchronized (studyRoom) {System.out.println("study 1 小時(shí)");Thread.sleep(1000);}}
}
活躍性
死鎖
形成
死鎖:多個(gè)線程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放,由于線程被無限期地阻塞,因此程序不可能正常終止
Java 死鎖產(chǎn)生的四個(gè)必要條件:
- 互斥條件,即當(dāng)資源被一個(gè)線程使用(占有)時(shí),別的線程不能使用
- 不可剝奪條件,資源請(qǐng)求者不能強(qiáng)制從資源占有者手中奪取資源,資源只能由資源占有者主動(dòng)釋放
- 請(qǐng)求和保持條件,即當(dāng)資源請(qǐng)求者在請(qǐng)求其他的資源的同時(shí)保持對(duì)原有資源的占有
- 循環(huán)等待條件,即存在一個(gè)等待循環(huán)隊(duì)列:p1 要 p2 的資源,p2 要 p1 的資源,形成了一個(gè)等待環(huán)路
四個(gè)條件都成立的時(shí)候,便形成死鎖。死鎖情況下打破上述任何一個(gè)條件,便可讓死鎖消失
public class Dead {public static Object resources1 = new Object();public static Object resources2 = new Object();public static void main(String[] args) {new Thread(() -> {// 線程1:占用資源1 ,請(qǐng)求資源2synchronized(resources1){System.out.println("線程1已經(jīng)占用了資源1,開始請(qǐng)求資源2");Thread.sleep(2000);//休息兩秒,防止線程1直接運(yùn)行完成。//2秒內(nèi)線程2肯定可以鎖住資源2synchronized (resources2){System.out.println("線程1已經(jīng)占用了資源2");}}).start();new Thread(() -> {// 線程2:占用資源2 ,請(qǐng)求資源1synchronized(resources2){System.out.println("線程2已經(jīng)占用了資源2,開始請(qǐng)求資源1");Thread.sleep(2000);synchronized (resources1){System.out.println("線程2已經(jīng)占用了資源1");}}}}).start();}
}
定位
定位死鎖的方法:
-
使用 jps 定位進(jìn)程 id,再用
jstack id
定位死鎖,找到死鎖的線程去查看源碼,解決優(yōu)化"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000]java.lang.Thread.State: BLOCKED (on object monitor) #省略 "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000]java.lang.Thread.State: BLOCKED (on object monitor) #省略Found one Java-level deadlock: =================================================== "Thread-1":waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),which is held by "Thread-0" "Thread-0":waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),which is held by "Thread-1"Java stack information for the threads listed above: =================================================== "Thread-1":at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)- locked <0x000000076b5bf1d0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)at java.lang.Thread.run(Thread.java:745) "Thread-0":at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)- locked <0x000000076b5bf1c0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$1/495053715
-
Linux 下可以通過 top 先定位到 CPU 占用高的 Java 進(jìn)程,再利用
top -Hp 進(jìn)程id
來定位是哪個(gè)線程,最后再用 jstack 的輸出來看各個(gè)線程棧 -
避免死鎖:避免死鎖要注意加鎖順序
-
可以使用 jconsole 工具,在
jdk\bin
目錄下
活鎖
活鎖:指的是任務(wù)或者執(zhí)行者沒有被阻塞,由于某些條件沒有滿足,導(dǎo)致一直重復(fù)嘗試—失敗—嘗試—失敗的過程
兩個(gè)線程互相改變對(duì)方的結(jié)束條件,最后誰也無法結(jié)束:
class TestLiveLock {static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {// 期望減到 0 退出循環(huán)while (count > 0) {Thread.sleep(200);count--;System.out.println("線程一count:" + count);}}, "t1").start();new Thread(() -> {// 期望超過 20 退出循環(huán)while (count < 20) {Thread.sleep(200);count++;System.out.println("線程二count:"+ count);}}, "t2").start();}
}
饑餓
饑餓:一個(gè)線程由于優(yōu)先級(jí)太低,始終得不到 CPU 調(diào)度執(zhí)行,也不能夠結(jié)束
wait-ify
基本使用
需要獲取對(duì)象鎖后才可以調(diào)用 鎖對(duì)象.wait()
,notify 隨機(jī)喚醒一個(gè)線程,notifyAll 喚醒所有線程去競(jìng)爭(zhēng) CPU
Object 類 API:
public final void notify():喚醒正在等待對(duì)象監(jiān)視器的單個(gè)線程。
public final void notifyAll():喚醒正在等待對(duì)象監(jiān)視器的所有線程。
public final void wait():導(dǎo)致當(dāng)前線程等待,直到另一個(gè)線程調(diào)用該對(duì)象的notify()方法或 notifyAll()方法。
public final native void wait(long timeout):有時(shí)限的等待, 到n毫秒后結(jié)束等待,或是被喚醒
說明:wait 是掛起線程,需要喚醒的都是掛起操作,阻塞線程可以自己去爭(zhēng)搶鎖,掛起的線程需要喚醒后去爭(zhēng)搶鎖
對(duì)比 sleep():
- 原理不同:sleep() 方法是屬于 Thread 類,是線程用來控制自身流程的,使此線程暫停執(zhí)行一段時(shí)間而把執(zhí)行機(jī)會(huì)讓給其他線程;wait() 方法屬于 Object 類,用于線程間通信
- 對(duì)鎖的處理機(jī)制不同:調(diào)用 sleep() 方法的過程中,線程不會(huì)釋放對(duì)象鎖,當(dāng)調(diào)用 wait() 方法的時(shí)候,線程會(huì)放棄對(duì)象鎖,進(jìn)入等待此對(duì)象的等待鎖定池(不釋放鎖其他線程怎么搶占到鎖執(zhí)行喚醒操作),但是都會(huì)釋放 CPU
- 使用區(qū)域不同:wait() 方法必須放在**同步控制方法和同步代碼塊(先獲取鎖)**中使用,sleep() 方法則可以放在任何地方使用
底層原理:
- Owner 線程發(fā)現(xiàn)條件不滿足,調(diào)用 wait 方法,即可進(jìn)入 WaitSet 變?yōu)?WAITING 狀態(tài)
- BLOCKED 和 WAITING 的線程都處于阻塞狀態(tài),不占用 CPU 時(shí)間片
- BLOCKED 線程會(huì)在 Owner 線程釋放鎖時(shí)喚醒
- WAITING 線程會(huì)在 Owner 線程調(diào)用 notify 或 notifyAll 時(shí)喚醒,喚醒后并不意味者立刻獲得鎖,需要進(jìn)入 EntryList 重新競(jìng)爭(zhēng)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-hNvz9pH1-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png)]
代碼優(yōu)化
虛假喚醒:notify 只能隨機(jī)喚醒一個(gè) WaitSet 中的線程,這時(shí)如果有其它線程也在等待,那么就可能喚醒不了正確的線程
解決方法:采用 notifyAll
notifyAll 僅解決某個(gè)線程的喚醒問題,使用 if + wait 判斷僅有一次機(jī)會(huì),一旦條件不成立,無法重新判斷
解決方法:用 while + wait,當(dāng)條件不成立,再次 wait
@Slf4j(topic = "c.demo")
public class demo {static final Object room = new Object();static boolean hasCigarette = false; //有沒有煙static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room) {log.debug("有煙沒?[{}]", hasCigarette);while (!hasCigarette) {//while防止虛假喚醒log.debug("沒煙,先歇會(huì)!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有煙沒?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以開始干活了");} else {log.debug("沒干成活...");}}}, "小南").start();new Thread(() -> {synchronized (room) {Thread thread = Thread.currentThread();log.debug("外賣送到?jīng)]?[{}]", hasTakeout);if (!hasTakeout) {log.debug("沒外賣,先歇會(huì)!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("外賣送到?jīng)]?[{}]", hasTakeout);if (hasTakeout) {log.debug("可以開始干活了");} else {log.debug("沒干成活...");}}}, "小女").start();Thread.sleep(1000);new Thread(() -> {// 這里能不能加 synchronized (room)?synchronized (room) {hasTakeout = true;//log.debug("煙到了噢!");log.debug("外賣到了噢!");room.notifyAll();}}, "送外賣的").start();}
}
park-un
LockSupport 是用來創(chuàng)建鎖和其他同步類的線程原語
LockSupport 類方法:
LockSupport.park()
:暫停當(dāng)前線程,掛起原語LockSupport.unpark(暫停的線程對(duì)象)
:恢復(fù)某個(gè)線程的運(yùn)行
public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("start..."); //1Thread.sleep(1000);// Thread.sleep(3000)// 先 park 再 unpark 和先 unpark 再 park 效果一樣,都會(huì)直接恢復(fù)線程的運(yùn)行System.out.println("park..."); //2LockSupport.park();System.out.println("resume...");//4},"t1");t1.start();Thread.sleep(2000);System.out.println("unpark..."); //3LockSupport.unpark(t1);
}
LockSupport 出現(xiàn)就是為了增強(qiáng) wait & notify 的功能:
- wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park、unpark 不需要
- park & unpark 以線程為單位來阻塞和喚醒線程,而 notify 只能隨機(jī)喚醒一個(gè)等待線程,notifyAll 是喚醒所有等待線程
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify。類比生產(chǎn)消費(fèi),先消費(fèi)發(fā)現(xiàn)有產(chǎn)品就消費(fèi),沒有就等待;先生產(chǎn)就直接產(chǎn)生商品,然后線程直接消費(fèi)
- wait 會(huì)釋放鎖資源進(jìn)入等待隊(duì)列,park 不會(huì)釋放鎖資源,只負(fù)責(zé)阻塞當(dāng)前線程,會(huì)釋放 CPU
原理:類似生產(chǎn)者消費(fèi)者
- 先 park:
- 當(dāng)前線程調(diào)用 Unsafe.park() 方法
- 檢查 _counter ,本情況為 0,這時(shí)獲得 _mutex 互斥鎖
- 線程進(jìn)入 _cond 條件變量掛起
- 調(diào)用 Unsafe.unpark(Thread_0) 方法,設(shè)置 _counter 為 1
- 喚醒 _cond 條件變量中的 Thread_0,Thread_0 恢復(fù)運(yùn)行,設(shè)置 _counter 為 0
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-seT6ZMdS-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理1.png)]
-
先 unpark:
- 調(diào)用 Unsafe.unpark(Thread_0) 方法,設(shè)置 _counter 為 1
- 當(dāng)前線程調(diào)用 Unsafe.park() 方法
- 檢查 _counter ,本情況為 1,這時(shí)線程無需掛起,繼續(xù)運(yùn)行,設(shè)置 _counter 為 0
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-MiHGw9PT-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理2.png)]
安全分析
成員變量和靜態(tài)變量:
- 如果它們沒有共享,則線程安全
- 如果它們被共享了,根據(jù)它們的狀態(tài)是否能夠改變,分兩種情況:
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區(qū),需要考慮線程安全問題
局部變量:
- 局部變量是線程安全的
- 局部變量引用的對(duì)象不一定線程安全(逃逸分析):
- 如果該對(duì)象沒有逃離方法的作用訪問,它是線程安全的(每一個(gè)方法有一個(gè)棧幀)
- 如果該對(duì)象逃離方法的作用范圍,需要考慮線程安全問題(暴露引用)
常見線程安全類:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包
-
線程安全的是指,多個(gè)線程調(diào)用它們同一個(gè)實(shí)例的某個(gè)方法時(shí),是線程安全的
-
每個(gè)方法是原子的,但多個(gè)方法的組合不是原子的,只能保證調(diào)用的方法內(nèi)部安全:
Hashtable table = new Hashtable(); // 線程1,線程2 if(table.get("key") == null) {table.put("key", value); }
無狀態(tài)類線程安全,就是沒有成員變量的類
不可變類線程安全:String、Integer 等都是不可變類,內(nèi)部的狀態(tài)不可以改變,所以方法是線程安全
-
replace 等方法底層是新建一個(gè)對(duì)象,復(fù)制過去
Map<String,Object> map = new HashMap<>(); // 線程不安全 String S1 = "..."; // 線程安全 final String S2 = "..."; // 線程安全 Date D1 = new Date(); // 線程不安全 final Date D2 = new Date(); // 線程不安全,final讓D2引用的對(duì)象不能變,但對(duì)象的內(nèi)容可以變
抽象方法如果有參數(shù),被重寫后行為不確定可能造成線程不安全,被稱之為外星方法:public abstract foo(Student s);
同步模式
保護(hù)性暫停
單任務(wù)版
Guarded Suspension,用在一個(gè)線程等待另一個(gè)線程的執(zhí)行結(jié)果
- 有一個(gè)結(jié)果需要從一個(gè)線程傳遞到另一個(gè)線程,讓它們關(guān)聯(lián)同一個(gè) GuardedObject
- 如果有結(jié)果不斷從一個(gè)線程到另一個(gè)線程那么可以使用消息隊(duì)列(見生產(chǎn)者/消費(fèi)者)
- JDK 中,join 的實(shí)現(xiàn)、Future 的實(shí)現(xiàn),采用的就是此模式
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-2pGNXQ7J-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保護(hù)性暫停.png)]
public static void main(String[] args) {GuardedObject object = new GuardedObjectV2();new Thread(() -> {sleep(1);object.complete(Arrays.asList("a", "b", "c"));}).start();Object response = object.get(2500);if (response != null) {log.debug("get response: [{}] lines", ((List<String>) response).size());} else {log.debug("can't get response");}
}class GuardedObject {private Object response;private final Object lock = new Object();//獲取結(jié)果//timeout :最大等待時(shí)間public Object get(long millis) {synchronized (lock) {// 1) 記錄最初時(shí)間long begin = System.currentTimeMillis();// 2) 已經(jīng)經(jīng)歷的時(shí)間long timePassed = 0;while (response == null) {// 4) 假設(shè) millis 是 1000,結(jié)果在 400 時(shí)喚醒了,那么還有 600 要等long waitTime = millis - timePassed;log.debug("waitTime: {}", waitTime);//經(jīng)歷時(shí)間超過最大等待時(shí)間退出循環(huán)if (waitTime <= 0) {log.debug("break...");break;}try {lock.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 3) 如果提前被喚醒,這時(shí)已經(jīng)經(jīng)歷的時(shí)間假設(shè)為 400timePassed = System.currentTimeMillis() - begin;log.debug("timePassed: {}, object is null {}",timePassed, response == null);}return response;}}//產(chǎn)生結(jié)果public void complete(Object response) {synchronized (lock) {// 條件滿足,通知等待線程this.response = response;log.debug("notify...");lock.notifyAll();}}
}
多任務(wù)版
多任務(wù)版保護(hù)性暫停:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-4AR92Rvf-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保護(hù)性暫停多任務(wù)版.png)]
public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 3; i++) {new People().start();}Thread.sleep(1000);for (Integer id : Mailboxes.getIds()) {new Postman(id, id + "號(hào)快遞到了").start();}
}@Slf4j(topic = "c.People")
class People extends Thread{@Overridepublic void run() {// 收信GuardedObject guardedObject = Mailboxes.createGuardedObject();log.debug("開始收信i d:{}", guardedObject.getId());Object mail = guardedObject.get(5000);log.debug("收到信id:{},內(nèi)容:{}", guardedObject.getId(),mail);}
}class Postman extends Thread{private int id;private String mail;//構(gòu)造方法@Overridepublic void run() {GuardedObject guardedObject = Mailboxes.getGuardedObject(id);log.debug("開始送信i d:{},內(nèi)容:{}", guardedObject.getId(),mail);guardedObject.complete(mail);}
}class Mailboxes {private static Map<Integer, GuardedObject> boxes = new Hashtable<>();private static int id = 1;//產(chǎn)生唯一的idprivate static synchronized int generateId() {return id++;}public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}public static GuardedObject createGuardedObject() {GuardedObject go = new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static Set<Integer> getIds() {return boxes.keySet();}
}
class GuardedObject {//標(biāo)識(shí),Guarded Objectprivate int id;//添加get set方法
}
順序輸出
順序輸出 2 1
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (true) {//try { Thread.sleep(1000); } catch (InterruptedException e) { }// 當(dāng)沒有許可時(shí),當(dāng)前線程暫停運(yùn)行;有許可時(shí),用掉這個(gè)許可,當(dāng)前線程恢復(fù)運(yùn)行LockSupport.park();System.out.println("1");}});Thread t2 = new Thread(() -> {while (true) {System.out.println("2");// 給線程 t1 發(fā)放『許可』(多次連續(xù)調(diào)用 unpark 只會(huì)發(fā)放一個(gè)『許可』)LockSupport.unpark(t1);try { Thread.sleep(500); } catch (InterruptedException e) { }}});t1.start();t2.start();
}
交替輸出
連續(xù)輸出 5 次 abc
public class day2_14 {public static void main(String[] args) throws InterruptedException {AwaitSignal awaitSignal = new AwaitSignal(5);Condition a = awaitSignal.newCondition();Condition b = awaitSignal.newCondition();Condition c = awaitSignal.newCondition();new Thread(() -> {awaitSignal.print("a", a, b);}).start();new Thread(() -> {awaitSignal.print("b", b, c);}).start();new Thread(() -> {awaitSignal.print("c", c, a);}).start();Thread.sleep(1000);awaitSignal.lock();try {a.signal();} finally {awaitSignal.unlock();}}
}class AwaitSignal extends ReentrantLock {private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber = loopNumber;}//參數(shù)1:打印內(nèi)容 參數(shù)二:條件變量 參數(shù)二:喚醒下一個(gè)public void print(String str, Condition condition, Condition next) {for (int i = 0; i < loopNumber; i++) {lock();try {condition.await();System.out.print(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {unlock();}}}
}
異步模式
傳統(tǒng)版
異步模式之生產(chǎn)者/消費(fèi)者:
class ShareData {private int number = 0;private Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();public void increment() throws Exception{// 同步代碼塊,加鎖lock.lock();try {// 判斷 防止虛假喚醒while(number != 0) {// 等待不能生產(chǎn)condition.await();}// 干活number++;System.out.println(Thread.currentThread().getName() + "\t " + number);// 通知 喚醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public void decrement() throws Exception{// 同步代碼塊,加鎖lock.lock();try {// 判斷 防止虛假喚醒while(number == 0) {// 等待不能消費(fèi)condition.await();}// 干活number--;System.out.println(Thread.currentThread().getName() + "\t " + number);// 通知 喚醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}
}public class TraditionalProducerConsumer {public static void main(String[] args) {ShareData shareData = new ShareData();// t1線程,生產(chǎn)new Thread(() -> {for (int i = 0; i < 5; i++) {shareData.increment();}}, "t1").start();// t2線程,消費(fèi)new Thread(() -> {for (int i = 0; i < 5; i++) {shareData.decrement();}}, "t2").start(); }
}
改進(jìn)版
異步模式之生產(chǎn)者/消費(fèi)者:
- 消費(fèi)隊(duì)列可以用來平衡生產(chǎn)和消費(fèi)的線程資源,不需要產(chǎn)生結(jié)果和消費(fèi)結(jié)果的線程一一對(duì)應(yīng)
- 生產(chǎn)者僅負(fù)責(zé)產(chǎn)生結(jié)果數(shù)據(jù),不關(guān)心數(shù)據(jù)該如何處理,而消費(fèi)者專心處理結(jié)果數(shù)據(jù)
- 消息隊(duì)列是有容量限制的,滿時(shí)不會(huì)再加入數(shù)據(jù),空時(shí)不會(huì)再消耗數(shù)據(jù)
- JDK 中各種阻塞隊(duì)列,采用的就是這種模式
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-75fSCNTw-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-生產(chǎn)者消費(fèi)者模式.png)]
public class demo {public static void main(String[] args) {MessageQueue queue = new MessageQueue(2);for (int i = 0; i < 3; i++) {int id = i;new Thread(() -> {queue.put(new Message(id,"值"+id));}, "生產(chǎn)者" + i).start();}new Thread(() -> {while (true) {try {Thread.sleep(1000);Message message = queue.take();} catch (InterruptedException e) {e.printStackTrace();}}},"消費(fèi)者").start();}
}//消息隊(duì)列類,Java間線程之間通信
class MessageQueue {private LinkedList<Message> list = new LinkedList<>();//消息的隊(duì)列集合private int capacity;//隊(duì)列容量public MessageQueue(int capacity) {this.capacity = capacity;}//獲取消息public Message take() {//檢查隊(duì)列是否為空synchronized (list) {while (list.isEmpty()) {try {sout(Thread.currentThread().getName() + ":隊(duì)列為空,消費(fèi)者線程等待");list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//從隊(duì)列的頭部獲取消息返回Message message = list.removeFirst();sout(Thread.currentThread().getName() + ":已消費(fèi)消息--" + message);list.notifyAll();return message;}}//存入消息public void put(Message message) {synchronized (list) {//檢查隊(duì)列是否滿while (list.size() == capacity) {try {sout(Thread.currentThread().getName()+":隊(duì)列為已滿,生產(chǎn)者線程等待");list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//將消息加入隊(duì)列尾部list.addLast(message);sout(Thread.currentThread().getName() + ":已生產(chǎn)消息--" + message);list.notifyAll();}}
}final class Message {private int id;private Object value;//get set
}
阻塞隊(duì)列
public static void main(String[] args) {ExecutorService consumer = Executors.newFixedThreadPool(1);ExecutorService producer = Executors.newFixedThreadPool(1);BlockingQueue<Integer> queue = new SynchronousQueue<>();producer.submit(() -> {try {System.out.println("生產(chǎn)...");Thread.sleep(1000);queue.put(10);} catch (InterruptedException e) {e.printStackTrace();}});consumer.submit(() -> {try {System.out.println("等待消費(fèi)...");Integer result = queue.take();System.out.println("結(jié)果為:" + result);} catch (InterruptedException e) {e.printStackTrace();}});
}
內(nèi)存
JMM
內(nèi)存模型
Java 內(nèi)存模型是 Java Memory Model(JMM),本身是一種抽象的概念,實(shí)際上并不存在,描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式
JMM 作用:
- 屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果
- 規(guī)定了線程和內(nèi)存之間的一些關(guān)系
根據(jù) JMM 的設(shè)計(jì),系統(tǒng)存在一個(gè)主內(nèi)存(Main Memory),Java 中所有變量都存儲(chǔ)在主存中,對(duì)于所有線程都是共享的;每條線程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對(duì)所有變量的操作都是先對(duì)變量進(jìn)行拷貝,然后在工作內(nèi)存中進(jìn)行,不能直接操作主內(nèi)存中的變量;線程之間無法相互直接訪問,線程間的通信(傳遞)必須通過主內(nèi)存來完成
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-w1eR8vM3-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM內(nèi)存模型.png)]
主內(nèi)存和工作內(nèi)存:
- 主內(nèi)存:計(jì)算機(jī)的內(nèi)存,也就是經(jīng)常提到的 8G 內(nèi)存,16G 內(nèi)存,存儲(chǔ)所有共享變量的值
- 工作內(nèi)存:存儲(chǔ)該線程使用到的共享變量在主內(nèi)存的的值的副本拷貝
JVM 和 JMM 之間的關(guān)系:JMM 中的主內(nèi)存、工作內(nèi)存與 JVM 中的 Java 堆、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分,這兩者基本上是沒有關(guān)系的,如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來:
- 主內(nèi)存主要對(duì)應(yīng)于 Java 堆中的對(duì)象實(shí)例數(shù)據(jù)部分,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域
- 從更低層次上說,主內(nèi)存直接對(duì)應(yīng)于物理硬件的內(nèi)存,工作內(nèi)存對(duì)應(yīng)寄存器和高速緩存
內(nèi)存交互
Java 內(nèi)存模型定義了 8 個(gè)操作來完成主內(nèi)存和工作內(nèi)存的交互操作,每個(gè)操作都是原子的
非原子協(xié)定:沒有被 volatile 修飾的 long、double 外,默認(rèn)按照兩次 32 位的操作
存交互.png)
- lock:作用于主內(nèi)存,將一個(gè)變量標(biāo)識(shí)為被一個(gè)線程獨(dú)占狀態(tài)(對(duì)應(yīng) monitorenter)
- unclock:作用于主內(nèi)存,將一個(gè)變量從獨(dú)占狀態(tài)釋放出來,釋放后的變量才可以被其他線程鎖定(對(duì)應(yīng) monitorexit)
- read:作用于主內(nèi)存,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中
- load:作用于工作內(nèi)存,在 read 之后執(zhí)行,把 read 得到的值放入工作內(nèi)存的變量副本中
- use:作用于工作內(nèi)存,把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)遇到一個(gè)使用到變量的操作時(shí)都要使用該指令
- assign:作用于工作內(nèi)存,把從執(zhí)行引擎接收到的一個(gè)值賦給工作內(nèi)存的變量
- store:作用于工作內(nèi)存,把工作內(nèi)存的一個(gè)變量的值傳送到主內(nèi)存中
- write:作用于主內(nèi)存,在 store 之后執(zhí)行,把 store 得到的值放入主內(nèi)存的變量中
參考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md
三大特性
可見性
可見性:是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值
存在不可見問題的根本原因是由于緩存的存在,線程持有的是共享變量的副本,無法感知其他線程對(duì)于共享變量的更改,導(dǎo)致讀取的值不是最新的。但是 final 修飾的變量是不可變的,就算有緩存,也不會(huì)存在不可見的問題
main 線程對(duì) run 變量的修改對(duì)于 t 線程不可見,導(dǎo)致了 t 線程無法停止:
static boolean run = true; //添加volatile
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 線程t不會(huì)如預(yù)想的停下來
}
原因:
- 初始狀態(tài), t 線程剛開始從主內(nèi)存讀取了 run 的值到工作內(nèi)存
- 因?yàn)?t 線程要頻繁從主內(nèi)存中讀取 run 的值,JIT 編譯器會(huì)將 run 的值緩存至自己工作內(nèi)存中的高速緩存中,減少對(duì)主存中 run 的訪問,提高效率
- 1 秒之后,main 線程修改了 run 的值,并同步至主存,而 t 是從自己工作內(nèi)存中的高速緩存中讀取這個(gè)變量的值,結(jié)果永遠(yuǎn)是舊值
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-8f3nOpHW-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-可見性例子.png)]
原子性
原子性:不可分割,完整性,也就是說某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí),中間不可以被分割,需要具體完成,要么同時(shí)成功,要么同時(shí)失敗,保證指令不會(huì)受到線程上下文切換的影響
定義原子操作的使用規(guī)則:
- 不允許 read 和 load、store 和 write 操作之一單獨(dú)出現(xiàn),必須順序執(zhí)行,但是不要求連續(xù)
- 不允許一個(gè)線程丟棄 assign 操作,必須同步回主存
- 不允許一個(gè)線程無原因地(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從工作內(nèi)存同步會(huì)主內(nèi)存中
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(assign 或者 load)的變量,即對(duì)一個(gè)變量實(shí)施 use 和 store 操作之前,必須先自行 assign 和 load 操作
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作,但 lock 操作可以被同一線程重復(fù)執(zhí)行多次,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù)的 unlock 操作,變量才會(huì)被解鎖,lock 和 unlock 必須成對(duì)出現(xiàn)
- 如果對(duì)一個(gè)變量執(zhí)行 lock 操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新從主存加載
- 如果一個(gè)變量事先沒有被 lock 操作鎖定,則不允許執(zhí)行 unlock 操作,也不允許去 unlock 一個(gè)被其他線程鎖定的變量
- 對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行 store 和 write 操作)
有序性
有序性:在本線程內(nèi)觀察,所有操作都是有序的;在一個(gè)線程觀察另一個(gè)線程,所有操作都是無序的,無序是因?yàn)榘l(fā)生了指令重排序
CPU 的基本工作是執(zhí)行存儲(chǔ)的指令序列,即程序,程序的執(zhí)行過程實(shí)際上是不斷地取出指令、分析指令、執(zhí)行指令的過程,為了提高性能,編譯器和處理器會(huì)對(duì)指令重排,一般分為以下三種:
源代碼 -> 編譯器優(yōu)化的重排 -> 指令并行的重排 -> 內(nèi)存系統(tǒng)的重排 -> 最終執(zhí)行指令
現(xiàn)代 CPU 支持多級(jí)指令流水線,幾乎所有的馮?諾伊曼型計(jì)算機(jī)的 CPU,其工作都可以分為 5 個(gè)階段:取指令、指令譯碼、執(zhí)行指令、訪存取數(shù)和結(jié)果寫回,可以稱之為五級(jí)指令流水線。CPU 可以在一個(gè)時(shí)鐘周期內(nèi),同時(shí)運(yùn)行五條指令的不同階段(每個(gè)線程不同的階段),本質(zhì)上流水線技術(shù)并不能縮短單條指令的執(zhí)行時(shí)間,但變相地提高了指令地吞吐率
處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴性
- 單線程環(huán)境也存在指令重排,由于存在依賴性,最終執(zhí)行結(jié)果和代碼順序的結(jié)果一致
- 多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排,會(huì)獲取其他線程處在不同階段的指令同時(shí)執(zhí)行
補(bǔ)充知識(shí):
- 指令周期是取出一條指令并執(zhí)行這條指令的時(shí)間,一般由若干個(gè)機(jī)器周期組成
- 機(jī)器周期也稱為 CPU 周期,一條指令的執(zhí)行過程劃分為若干個(gè)階段(如取指、譯碼、執(zhí)行等),每一階段完成一個(gè)基本操作,完成一個(gè)基本操作所需要的時(shí)間稱為機(jī)器周期
- 振蕩周期指周期性信號(hào)作周期性重復(fù)變化的時(shí)間間隔
cache
緩存機(jī)制
緩存結(jié)構(gòu)
在計(jì)算機(jī)系統(tǒng)中,CPU 高速緩存(CPU Cache,簡(jiǎn)稱緩存)是用于減少處理器訪問內(nèi)存所需平均時(shí)間的部件;在存儲(chǔ)體系中位于自頂向下的第二層,僅次于 CPU 寄存器;其容量遠(yuǎn)小于內(nèi)存,但速度卻可以接近處理器的頻率
CPU 處理器速度遠(yuǎn)遠(yuǎn)大于在主內(nèi)存中的,為了解決速度差異,在它們之間架設(shè)了多級(jí)緩存,如 L1、L2、L3 級(jí)別的緩存,這些緩存離 CPU 越近就越快,將頻繁操作的數(shù)據(jù)緩存到這里,加快訪問速度
構(gòu).png)
從 CPU 到 | 大約需要的時(shí)鐘周期 |
---|---|
寄存器 | 1 cycle (4GHz 的 CPU 約為 0.25ns) |
L1 | 3~4 cycle |
L2 | 10~20 cycle |
L3 | 40~45 cycle |
內(nèi)存 | 120~240 cycle |
緩存使用
當(dāng)處理器發(fā)出內(nèi)存訪問請(qǐng)求時(shí),會(huì)先查看緩存內(nèi)是否有請(qǐng)求數(shù)據(jù),如果存在(命中),則不用訪問內(nèi)存直接返回該數(shù)據(jù);如果不存在(失效),則要先把內(nèi)存中的相應(yīng)數(shù)據(jù)載入緩存,再將其返回處理器
緩存之所以有效,主要因?yàn)槌绦蜻\(yùn)行時(shí)對(duì)內(nèi)存的訪問呈現(xiàn)局部性(Locality)特征。既包括空間局部性(Spatial Locality),也包括時(shí)間局部性(Temporal Locality),有效利用這種局部性,緩存可以達(dá)到極高的命中率
偽共享
緩存以緩存行 cache line 為單位,每個(gè)緩存行對(duì)應(yīng)著一塊內(nèi)存,一般是 64 byte(8 個(gè) long),在 CPU 從主存獲取數(shù)據(jù)時(shí),以 cache line 為單位加載,于是相鄰的數(shù)據(jù)會(huì)一并加載到緩存中
緩存會(huì)造成數(shù)據(jù)副本的產(chǎn)生,即同一份數(shù)據(jù)會(huì)緩存在不同核心的緩存行中,CPU 要保證數(shù)據(jù)的一致性,需要做到某個(gè) CPU 核心更改了數(shù)據(jù),其它 CPU 核心對(duì)應(yīng)的整個(gè)緩存行必須失效,這就是偽共享
解決方法:
-
padding:通過填充,讓數(shù)據(jù)落在不同的 cache line 中
-
@Contended:原理參考 無鎖 → Adder → 優(yōu)化機(jī)制 → 偽共享
Linux 查看 CPU 緩存行:
- 命令:
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64
- 內(nèi)存地址格式:[高位組標(biāo)記] [低位索引] [偏移量]
緩存一致
緩存一致性:當(dāng)多個(gè)處理器運(yùn)算任務(wù)都涉及到同一塊主內(nèi)存區(qū)域的時(shí)候,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一樣

MESI(Modified Exclusive Shared Or Invalid)是一種廣泛使用的支持寫回策略的緩存一致性協(xié)議,CPU 中每個(gè)緩存行(caceh line)使用 4 種狀態(tài)進(jìn)行標(biāo)記(使用額外的兩位 bit 表示):
-
M:被修改(Modified)
該緩存行只被緩存在該 CPU 的緩存中,并且是被修改過的,與主存中的數(shù)據(jù)不一致 (dirty),該緩存行中的內(nèi)存需要寫回 (write back) 主存。該狀態(tài)的數(shù)據(jù)再次被修改不會(huì)發(fā)送廣播,因?yàn)槠渌诵牡臄?shù)據(jù)已經(jīng)在第一次修改時(shí)失效一次
當(dāng)被寫回主存之后,該緩存行的狀態(tài)會(huì)變成獨(dú)享 (exclusive) 狀態(tài)
-
E:獨(dú)享的(Exclusive)
該緩存行只被緩存在該 CPU 的緩存中,是未被修改過的 (clear),與主存中數(shù)據(jù)一致,修改數(shù)據(jù)不需要通知其他 CPU 核心,該狀態(tài)可以在任何時(shí)刻有其它 CPU 讀取該內(nèi)存時(shí)變成共享狀態(tài) (shared)
當(dāng) CPU 修改該緩存行中內(nèi)容時(shí),該狀態(tài)可以變成 Modified 狀態(tài)
-
S:共享的(Shared)
該狀態(tài)意味著該緩存行可能被多個(gè) CPU 緩存,并且各個(gè)緩存中的數(shù)據(jù)與主存數(shù)據(jù)一致,當(dāng) CPU 修改該緩存行中,會(huì)向其它 CPU 核心廣播一個(gè)請(qǐng)求,使該緩存行變成無效狀態(tài) (Invalid),然后再更新當(dāng)前 Cache 里的數(shù)據(jù)
-
I:無效的(Invalid)
該緩存是無效的,可能有其它 CPU 修改了該緩存行
解決方法:各個(gè)處理器訪問緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議進(jìn)行操作,協(xié)議主要有 MSI、MESI 等
處理機(jī)制
單核 CPU 處理器會(huì)自動(dòng)保證基本內(nèi)存操作的原子性
多核 CPU 處理器,每個(gè) CPU 處理器內(nèi)維護(hù)了一塊內(nèi)存,每個(gè)內(nèi)核內(nèi)部維護(hù)著一塊緩存,當(dāng)多線程并發(fā)讀寫時(shí),就會(huì)出現(xiàn)緩存數(shù)據(jù)不一致的情況。處理器提供:
- 總線鎖定:當(dāng)處理器要操作共享變量時(shí),在 BUS 總線上發(fā)出一個(gè) LOCK 信號(hào),其他處理器就無法操作這個(gè)共享變量,該操作會(huì)導(dǎo)致大量阻塞,從而增加系統(tǒng)的性能開銷(平臺(tái)級(jí)別的加鎖)
- 緩存鎖定:當(dāng)處理器對(duì)緩存中的共享變量進(jìn)行了操作,其他處理器有嗅探機(jī)制,將各自緩存中的該共享變量的失效,讀取時(shí)會(huì)重新從主內(nèi)存中讀取最新的數(shù)據(jù),基于 MESI 緩存一致性協(xié)議來實(shí)現(xiàn)
有如下兩種情況處理器不會(huì)使用緩存鎖定:
-
當(dāng)操作的數(shù)據(jù)跨多個(gè)緩存行,或沒被緩存在處理器內(nèi)部,則處理器會(huì)使用總線鎖定
-
有些處理器不支持緩存鎖定,比如:Intel 486 和 Pentium 處理器也會(huì)調(diào)用總線鎖定
總線機(jī)制:
-
總線嗅探:每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存值是否過期了,當(dāng)處理器發(fā)現(xiàn)自己的緩存對(duì)應(yīng)的內(nèi)存地址的數(shù)據(jù)被修改,就將當(dāng)前處理器的緩存行設(shè)置為無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行操作時(shí),會(huì)重新從內(nèi)存中把數(shù)據(jù)讀取到處理器緩存中
-
總線風(fēng)暴:當(dāng)某個(gè) CPU 核心更新了 Cache 中的數(shù)據(jù),要把該事件廣播通知到其他核心(寫傳播),CPU 需要每時(shí)每刻監(jiān)聽總線上的一切活動(dòng),但是不管別的核心的 Cache 是否緩存相同的數(shù)據(jù),都需要發(fā)出一個(gè)廣播事件,不斷的從主內(nèi)存嗅探和 CAS 循環(huán),無效的交互會(huì)導(dǎo)致總線帶寬達(dá)到峰值;因此不要大量使用 volatile 關(guān)鍵字,使用 volatile、syschonized 都需要根據(jù)實(shí)際場(chǎng)景
volatile
同步機(jī)制
volatile 是 Java 虛擬機(jī)提供的輕量級(jí)的同步機(jī)制(三大特性)
- 保證可見性
- 不保證原子性
- 保證有序性(禁止指令重排)
性能:volatile 修飾的變量進(jìn)行讀操作與普通變量幾乎沒什么差別,但是寫操作相對(duì)慢一些,因?yàn)樾枰诒镜卮a中插入很多內(nèi)存屏障來保證指令不會(huì)發(fā)生亂序執(zhí)行,但是開銷比鎖要小
synchronized 無法禁止指令重排和處理器優(yōu)化,為什么可以保證有序性可見性
- 加了鎖之后,只能有一個(gè)線程獲得到了鎖,獲得不到鎖的線程就要阻塞,所以同一時(shí)間只有一個(gè)線程執(zhí)行,相當(dāng)于單線程,由于數(shù)據(jù)依賴性的存在,單線程的指令重排是沒有問題的
- 線程加鎖前,將清空工作內(nèi)存中共享變量的值,使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值;線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中(JMM 內(nèi)存交互章節(jié)有講)
指令重排
volatile 修飾的變量,可以禁用指令重排
指令重排實(shí)例:
-
example 1:
public void mySort() {int x = 11; //語句1int y = 12; //語句2 誰先執(zhí)行效果一樣x = x + 5; //語句3y = x * x; //語句4 }
執(zhí)行順序是:1 2 3 4、2 1 3 4、1 3 2 4
指令重排也有限制不會(huì)出現(xiàn):4321,語句 4 需要依賴于 y 以及 x 的申明,因?yàn)榇嬖跀?shù)據(jù)依賴,無法首先執(zhí)行
-
example 2:
int num = 0; boolean ready = false; // 線程1 執(zhí)行此方法 public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;} } // 線程2 執(zhí)行此方法 public void actor2(I_Result r) {num = 2;ready = true; }
情況一:線程 1 先執(zhí)行,ready = false,結(jié)果為 r.r1 = 1
情況二:線程 2 先執(zhí)行 num = 2,但還沒執(zhí)行 ready = true,線程 1 執(zhí)行,結(jié)果為 r.r1 = 1
情況三:線程 2 先執(zhí)行 ready = true,線程 1 執(zhí)行,進(jìn)入 if 分支結(jié)果為 r.r1 = 4
情況四:線程 2 執(zhí)行 ready = true,切換到線程 1,進(jìn)入 if 分支為 r.r1 = 0,再切回線程 2 執(zhí)行 num = 2,發(fā)生指令重排
底層原理
緩存一致
使用 volatile 修飾的共享變量,底層通過匯編 lock 前綴指令進(jìn)行緩存鎖定,在線程修改完共享變量后寫回主存,其他的 CPU 核心上運(yùn)行的線程通過 CPU 總線嗅探機(jī)制會(huì)修改其共享變量為失效狀態(tài),讀取時(shí)會(huì)重新從主內(nèi)存中讀取最新的數(shù)據(jù)
lock 前綴指令就相當(dāng)于內(nèi)存屏障,Memory Barrier(Memory Fence)
- 對(duì) volatile 變量的寫指令后會(huì)加入寫屏障
- 對(duì) volatile 變量的讀指令前會(huì)加入讀屏障
內(nèi)存屏障有三個(gè)作用:
- 確保對(duì)內(nèi)存的讀-改-寫操作原子執(zhí)行
- 阻止屏障兩側(cè)的指令重排序
- 強(qiáng)制把緩存中的臟數(shù)據(jù)寫回主內(nèi)存,讓緩存行中相應(yīng)的數(shù)據(jù)失效
內(nèi)存屏障
保證可見性:
-
寫屏障(sfence,Store Barrier)保證在該屏障之前的,對(duì)共享變量的改動(dòng),都同步到主存當(dāng)中
public void actor2(I_Result r) {num = 2;ready = true; // ready 是 volatile 賦值帶寫屏障// 寫屏障 }
-
讀屏障(lfence,Load Barrier)保證在該屏障之后的,對(duì)共享變量的讀取,從主存刷新變量值,加載的是主存中最新數(shù)據(jù)
public void actor1(I_Result r) {// 讀屏障// ready 是 volatile 讀取值帶讀屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;} }
-
全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能
保證有序性:
- 寫屏障會(huì)確保指令重排序時(shí),不會(huì)將寫屏障之前的代碼排在寫屏障之后
- 讀屏障會(huì)確保指令重排序時(shí),不會(huì)將讀屏障之后的代碼排在讀屏障之前
不能解決指令交錯(cuò):
-
寫屏障僅僅是保證之后的讀能夠讀到最新的結(jié)果,但不能保證其他線程的讀跑到寫屏障之前
-
有序性的保證也只是保證了本線程內(nèi)相關(guān)代碼不被重排序
volatile i = 0; new Thread(() -> {i++}); new Thread(() -> {i--});
i++ 反編譯后的指令:
0: iconst_1 // 當(dāng)int取值 -1~5 時(shí),JVM采用iconst指令將常量壓入棧中 1: istore_1 // 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表的 slot 1 2: iinc 1, 1
交互規(guī)則
對(duì)于 volatile 修飾的變量:
- 線程對(duì)變量的 use 與 load、read 操作是相關(guān)聯(lián)的,所以變量使用前必須先從主存加載
- 線程對(duì)變量的 assign 與 store、write 操作是相關(guān)聯(lián)的,所以變量使用后必須同步至主存
- 線程 1 和線程 2 誰先對(duì)變量執(zhí)行 read 操作,就會(huì)先進(jìn)行 write 操作,防止指令重排
雙端檢鎖
檢鎖機(jī)制
Double-Checked Locking:雙端檢鎖機(jī)制
DCL(雙端檢鎖)機(jī)制不一定是線程安全的,原因是有指令重排的存在,加入 volatile 可以禁止指令重排
public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {if(INSTANCE == null) { // t2,這里的判斷不是線程安全的// 首次訪問會(huì)同步,而之后的使用沒有 synchronizedsynchronized(Singleton.class) {// 這里是線程安全的判斷,防止其他線程在當(dāng)前線程等待鎖的期間完成了初始化if (INSTANCE == null) { INSTANCE = new Singleton();}}}return INSTANCE;}
}
不鎖 INSTANCE 的原因:
- INSTANCE 要重新賦值
- INSTANCE 是 null,線程加鎖之前需要獲取對(duì)象的引用,設(shè)置對(duì)象頭,null 沒有引用
實(shí)現(xiàn)特點(diǎn):
- 懶惰初始化
- 首次使用 getInstance() 才使用 synchronized 加鎖,后續(xù)使用時(shí)無需加鎖
- 第一個(gè) if 使用了 INSTANCE 變量,是在同步塊之外,但在多線程環(huán)境下會(huì)產(chǎn)生問題
DCL問題
getInstance 方法對(duì)應(yīng)的字節(jié)碼為:
0: getstatic #2 // Field INSTANCE:Ltest/Singleton;
3: ifnonnull 37
6: ldc #3 // class test/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Ltest/Singleton;
14: ifnonnull 27
17: new #3 // class test/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Ltest/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Ltest/Singleton;
40: areturn
- 17 表示創(chuàng)建對(duì)象,將對(duì)象引用入棧
- 20 表示復(fù)制一份對(duì)象引用,引用地址
- 21 表示利用一個(gè)對(duì)象引用,調(diào)用構(gòu)造方法初始化對(duì)象
- 24 表示利用一個(gè)對(duì)象引用,賦值給 static INSTANCE
步驟 21 和 24 之間不存在數(shù)據(jù)依賴關(guān)系,而且無論重排前后,程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的
- 關(guān)鍵在于 0:getstatic 這行代碼在 monitor 控制之外,可以越過 monitor 讀取 INSTANCE 變量的值
- 當(dāng)其他線程訪問 INSTANCE 不為 null 時(shí),由于 INSTANCE 實(shí)例未必已初始化,那么 t2 拿到的是將是一個(gè)未初始化完畢的單例返回,這就造成了線程安全的問題
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-HM8H5kL3-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL出現(xiàn)的問題.png)]
解決方法
指令重排只會(huì)保證串行語義的執(zhí)行一致性(單線程),但并不會(huì)關(guān)系多線程間的語義一致性
引入 volatile,來保證出現(xiàn)指令重排的問題,從而保證單例模式的線程安全性:
private static volatile SingletonDemo INSTANCE = null;
ha-be
happens-before 先行發(fā)生
Java 內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何同步手段(volatile、synchronized 等)就能夠得到保證的安全,這個(gè)通常也稱為 happens-before 原則,它是可見性與有序性的一套規(guī)則總結(jié)
不符合 happens-before 規(guī)則,JMM 并不能保證一個(gè)線程的可見性和有序性
-
程序次序規(guī)則 (Program Order Rule):一個(gè)線程內(nèi),邏輯上書寫在前面的操作先行發(fā)生于書寫在后面的操作 ,因?yàn)槎鄠€(gè)操作之間有先后依賴關(guān)系,則不允許對(duì)這些操作進(jìn)行重排序
-
鎖定規(guī)則 (Monitor Lock Rule):一個(gè) unlock 操作先行發(fā)生于后面(時(shí)間的先后)對(duì)同一個(gè)鎖的 lock 操作,所以線程解鎖 m 之前對(duì)變量的寫(解鎖前會(huì)刷新到主內(nèi)存中),對(duì)于接下來對(duì) m 加鎖的其它線程對(duì)該變量的讀可見
-
volatile 變量規(guī)則 (Volatile Variable Rule):對(duì) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀
-
傳遞規(guī)則 (Transitivity):具有傳遞性,如果操作 A 先行發(fā)生于操作 B,而操作 B 又先行發(fā)生于操作 C,則可以得出操作 A 先行發(fā)生于操作 C
-
線程啟動(dòng)規(guī)則 (Thread Start Rule):Thread 對(duì)象的 start()方 法先行發(fā)生于此線程中的每一個(gè)操作
static int x = 10;//線程 start 前對(duì)變量的寫,對(duì)該線程開始后對(duì)該變量的讀可見 new Thread(()->{ System.out.println(x); },"t1").start();
-
線程中斷規(guī)則 (Thread Interruption Rule):對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
-
線程終止規(guī)則 (Thread Termination Rule):線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),可以通過 Thread.join() 方法結(jié)束、Thread.isAlive() 的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
-
對(duì)象終結(jié)規(guī)則(Finaizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始
設(shè)計(jì)模式
終止模式
終止模式之兩階段終止模式:停止標(biāo)記用 volatile 是為了保證該變量在多個(gè)線程之間的可見性
class TwoPhaseTermination {// 監(jiān)控線程private Thread monitor;// 停止標(biāo)記private volatile boolean stop = false;;// 啟動(dòng)監(jiān)控線程public void start() {monitor = new Thread(() -> {while (true) {Thread thread = Thread.currentThread();if (stop) {System.out.println("后置處理");break;}try {Thread.sleep(1000);// 睡眠System.out.println(thread.getName() + "執(zhí)行監(jiān)控記錄");} catch (InterruptedException e) {System.out.println("被打斷,退出睡眠");}}});monitor.start();}// 停止監(jiān)控線程public void stop() {stop = true;monitor.interrupt();// 讓線程盡快退出Timed Waiting}
}
// 測(cè)試
public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt = new TwoPhaseTermination();tpt.start();Thread.sleep(3500);System.out.println("停止監(jiān)控");tpt.stop();
}
Balking
Balking (猶豫)模式用在一個(gè)線程發(fā)現(xiàn)另一個(gè)線程或本線程已經(jīng)做了某一件相同的事,那么本線程就無需再做了,直接結(jié)束返回
public class MonitorService {// 用來表示是否已經(jīng)有線程已經(jīng)在執(zhí)行啟動(dòng)了private volatile boolean starting = false;public void start() {System.out.println("嘗試啟動(dòng)監(jiān)控線程...");synchronized (this) {if (starting) {return;}starting = true;}// 真正啟動(dòng)監(jiān)控線程...}
}
對(duì)比保護(hù)性暫停模式:保護(hù)性暫停模式用在一個(gè)線程等待另一個(gè)線程的執(zhí)行結(jié)果,當(dāng)條件不滿足時(shí)線程等待
例子:希望 doInit() 方法僅被調(diào)用一次,下面的實(shí)現(xiàn)出現(xiàn)的問題:
- 當(dāng) t1 線程進(jìn)入 init() 準(zhǔn)備 doInit(),t2 線程進(jìn)來,initialized 還為f alse,則 t2 就又初始化一次
- volatile 適合一個(gè)線程寫,其他線程讀的情況,這個(gè)代碼需要加鎖
public class TestVolatile {volatile boolean initialized = false;void init() {if (initialized) {return;}doInit();initialized = true;}private void doInit() {}
}
無鎖
CAS
原理
無鎖編程:Lock Free
CAS 的全稱是 Compare-And-Swap,是 CPU 并發(fā)原語
- CAS 并發(fā)原語體現(xiàn)在 Java 語言中就是 sun.misc.Unsafe 類的各個(gè)方法,調(diào)用 UnSafe 類中的 CAS 方法,JVM 會(huì)實(shí)現(xiàn)出 CAS 匯編指令,這是一種完全依賴于硬件的功能,實(shí)現(xiàn)了原子操作
- CAS 是一種系統(tǒng)原語,原語屬于操作系統(tǒng)范疇,是由若干條指令組成 ,用于完成某個(gè)功能的一個(gè)過程,并且原語的執(zhí)行必須是連續(xù)的,執(zhí)行過程中不允許被中斷,所以 CAS 是一條 CPU 的原子指令,不會(huì)造成數(shù)據(jù)不一致的問題,是線程安全的
底層原理:CAS 的底層是 lock cmpxchg
指令(X86 架構(gòu)),在單核和多核 CPU 下都能夠保證比較交換的原子性
-
程序是在單核處理器上運(yùn)行,會(huì)省略 lock 前綴,單處理器自身會(huì)維護(hù)處理器內(nèi)的順序一致性,不需要 lock 前綴的內(nèi)存屏障效果
-
程序是在多核處理器上運(yùn)行,會(huì)為 cmpxchg 指令加上 lock 前綴。當(dāng)某個(gè)核執(zhí)行到帶 lock 的指令時(shí),CPU 會(huì)執(zhí)行總線鎖定或緩存鎖定,將修改的變量寫入到主存,這個(gè)過程不會(huì)被線程的調(diào)度機(jī)制所打斷,保證了多個(gè)線程對(duì)內(nèi)存操作的原子性
作用:比較當(dāng)前工作內(nèi)存中的值和主物理內(nèi)存中的值,如果相同則執(zhí)行規(guī)定操作,否則繼續(xù)比較直到主內(nèi)存和工作內(nèi)存的值一致為止
CAS 特點(diǎn):
- CAS 體現(xiàn)的是無鎖并發(fā)、無阻塞并發(fā),線程不會(huì)陷入阻塞,線程不需要頻繁切換狀態(tài)(上下文切換,系統(tǒng)調(diào)用)
- CAS 是基于樂觀鎖的思想
CAS 缺點(diǎn):
- 執(zhí)行的是循環(huán)操作,如果比較不成功一直在循環(huán),最差的情況某個(gè)線程一直取到的值和預(yù)期值都不一樣,就會(huì)無限循環(huán)導(dǎo)致饑餓,使用 CAS 線程數(shù)不要超過 CPU 的核心數(shù),采用分段 CAS 和自動(dòng)遷移機(jī)制
- 只能保證一個(gè)共享變量的原子操作
- 對(duì)于一個(gè)共享變量執(zhí)行操作時(shí),可以通過循環(huán) CAS 的方式來保證原子操作
- 對(duì)于多個(gè)共享變量操作時(shí),循環(huán) CAS 就無法保證操作的原子性,這個(gè)時(shí)候只能用鎖來保證原子性
- 引出來 ABA 問題
樂觀鎖
CAS 與 synchronized 總結(jié):
- synchronized 是從悲觀的角度出發(fā):總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞(共享資源每次只給一個(gè)線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程),因此 synchronized 也稱之為悲觀鎖,ReentrantLock 也是一種悲觀鎖,性能較差
- CAS 是從樂觀的角度出發(fā):總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù)。如果別人修改過,則獲取現(xiàn)在最新的值,如果別人沒修改過,直接修改共享數(shù)據(jù)的值,CAS 這種機(jī)制也稱之為樂觀鎖,綜合性能較好
Atomic
常用API
常見原子類:AtomicInteger、AtomicBoolean、AtomicLong
構(gòu)造方法:
public AtomicInteger()
:初始化一個(gè)默認(rèn)值為 0 的原子型 Integerpublic AtomicInteger(int initialValue)
:初始化一個(gè)指定值的原子型 Integer
常用API:
方法 | 作用 |
---|---|
public final int get() | 獲取 AtomicInteger 的值 |
public final int getAndIncrement() | 以原子方式將當(dāng)前值加 1,返回的是自增前的值 |
public final int incrementAndGet() | 以原子方式將當(dāng)前值加 1,返回的是自增后的值 |
public final int getAndSet(int value) | 以原子方式設(shè)置為 newValue 的值,返回舊值 |
public final int addAndGet(int data) | 以原子方式將輸入的數(shù)值與實(shí)例中的值相加并返回 實(shí)例:AtomicInteger 里的 value |
原理分析
AtomicInteger 原理:自旋鎖 + CAS 算法
CAS 算法:有 3 個(gè)操作數(shù)(內(nèi)存值 V, 舊的預(yù)期值 A,要修改的值 B)
- 當(dāng)舊的預(yù)期值 A == 內(nèi)存值 V 此時(shí)可以修改,將 V 改為 B
- 當(dāng)舊的預(yù)期值 A != 內(nèi)存值 V 此時(shí)不能修改,并重新獲取現(xiàn)在的最新值,重新獲取的動(dòng)作就是自旋
分析 getAndSet 方法:
-
AtomicInteger:
public final int getAndSet(int newValue) {/*** this: 當(dāng)前對(duì)象* valueOffset: 內(nèi)存偏移量,內(nèi)存地址*/return unsafe.getAndSetInt(this, valueOffset, newValue); }
valueOffset:偏移量表示該變量值相對(duì)于當(dāng)前對(duì)象地址的偏移,Unsafe 就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); //調(diào)用本地方法 --> public native long objectFieldOffset(Field var1);
-
unsafe 類:
// val1: AtomicInteger對(duì)象本身,var2: 該對(duì)象值得引用地址,var4: 需要變動(dòng)的數(shù) public final int getAndSetInt(Object var1, long var2, int var4) {int var5;do {// var5: 用 var1 和 var2 找到的內(nèi)存中的真實(shí)值var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var4));return var5; }
var5:從主內(nèi)存中拷貝到工作內(nèi)存中的值(每次都要從主內(nèi)存拿到最新的值到本地內(nèi)存),然后執(zhí)行
compareAndSwapInt()
再和主內(nèi)存的值進(jìn)行比較,假設(shè)方法返回 false,那么就一直執(zhí)行 while 方法,直到期望的值和真實(shí)值一樣,修改數(shù)據(jù) -
變量 value 用 volatile 修飾,保證了多線程之間的內(nèi)存可見性,避免線程從工作緩存中獲取失效的變量
private volatile int value
CAS 必須借助 volatile 才能讀取到共享變量的最新值來實(shí)現(xiàn)比較并交換的效果
分析 getAndUpdate 方法:
-
getAndUpdate:
public final int getAndUpdate(IntUnaryOperator updateFunction) {int prev, next;do {prev = get(); //當(dāng)前值,cas的期望值next = updateFunction.applyAsInt(prev);//期望值更新到該值} while (!compareAndSet(prev, next));//自旋return prev; }
函數(shù)式接口:可以自定義操作邏輯
AtomicInteger a = new AtomicInteger(); a.getAndUpdate(i -> i + 10);
-
compareAndSet:
public final boolean compareAndSet(int expect, int update) {/*** this: 當(dāng)前對(duì)象* valueOffset: 內(nèi)存偏移量,內(nèi)存地址* expect: 期望的值* update: 更新的值*/return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
原子引用
原子引用:對(duì) Object 進(jìn)行原子操作,提供一種讀和寫都是原子性的對(duì)象引用變量
原子引用類:AtomicReference、AtomicStampedReference、AtomicMarkableReference
AtomicReference 類:
-
構(gòu)造方法:
AtomicReference<T> atomicReference = new AtomicReference<T>()
-
常用 API:
public final boolean compareAndSet(V expectedValue, V newValue)
:CAS 操作public final void set(V newValue)
:將值設(shè)置為 newValuepublic final V get()
:返回當(dāng)前值
public class AtomicReferenceDemo {public static void main(String[] args) {Student s1 = new Student(33, "z3");// 創(chuàng)建原子引用包裝類AtomicReference<Student> atomicReference = new AtomicReference<>();// 設(shè)置主內(nèi)存共享變量為s1atomicReference.set(s1);// 比較并交換,如果現(xiàn)在主物理內(nèi)存的值為 z3,那么交換成 l4while (true) {Student s2 = new Student(44, "l4");if (atomicReference.compareAndSet(s1, s2)) {break;}}System.out.println(atomicReference.get());}
}class Student {private int id;private String name;//。。。。
}
原子數(shù)組
原子數(shù)組類:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
AtomicIntegerArray 類方法:
/**
* i the index
* expect the expected value
* update the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
原子更新器
原子更新器類:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
利用字段更新器,可以針對(duì)對(duì)象的某個(gè)域(Field)進(jìn)行原子操作,只能配合 volatile 修飾的字段使用,否則會(huì)出現(xiàn)異常 IllegalArgumentException: Must be volatile type
常用 API:
static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName)
:構(gòu)造方法abstract boolean compareAndSet(T obj, int expect, int update)
:CAS
public class UpdateDemo {private volatile int field;public static void main(String[] args) {AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(UpdateDemo.class, "field");UpdateDemo updateDemo = new UpdateDemo();fieldUpdater.compareAndSet(updateDemo, 0, 10);System.out.println(updateDemo.field);//10}
}
原子累加器
原子累加器類:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
LongAdder 和 LongAccumulator 區(qū)別:
相同點(diǎn):
- LongAddr 與 LongAccumulator 類都是使用非阻塞算法 CAS 實(shí)現(xiàn)的
- LongAddr 類是 LongAccumulator 類的一個(gè)特例,只是 LongAccumulator 提供了更強(qiáng)大的功能,可以自定義累加規(guī)則,當(dāng)accumulatorFunction 為 null 時(shí)就等價(jià)于 LongAddr
不同點(diǎn):
-
調(diào)用 casBase 時(shí),LongAccumulator 使用 function.applyAsLong(b = base, x) 來計(jì)算,LongAddr 使用 casBase(b = base, b + x)
-
LongAccumulator 類功能更加強(qiáng)大,構(gòu)造方法參數(shù)中
- accumulatorFunction 是一個(gè)雙目運(yùn)算器接口,可以指定累加規(guī)則,比如累加或者相乘,其根據(jù)輸入的兩個(gè)參數(shù)返回一個(gè)計(jì)算值,LongAdder 內(nèi)置累加規(guī)則
- identity 則是 LongAccumulator 累加器的初始值,LongAccumulator 可以為累加器提供非0的初始值,而 LongAdder 只能提供默認(rèn)的 0
Adder
優(yōu)化機(jī)制
LongAdder 是 Java8 提供的類,跟 AtomicLong 有相同的效果,但對(duì) CAS 機(jī)制進(jìn)行了優(yōu)化,嘗試使用分段 CAS 以及自動(dòng)分段遷移的方式來大幅度提升多線程高并發(fā)執(zhí)行 CAS 操作的性能
CAS 底層實(shí)現(xiàn)是在一個(gè)循環(huán)中不斷地嘗試修改目標(biāo)值,直到修改成功。如果競(jìng)爭(zhēng)不激烈修改成功率很高,否則失敗率很高,失敗后這些重復(fù)的原子性操作會(huì)耗費(fèi)性能(導(dǎo)致大量線程空循環(huán),自旋轉(zhuǎn))
優(yōu)化核心思想:數(shù)據(jù)分離,將 AtomicLong 的單點(diǎn)的更新壓力分擔(dān)到各個(gè)節(jié)點(diǎn),空間換時(shí)間,在低并發(fā)的時(shí)候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并發(fā)的時(shí)候通過分散減少競(jìng)爭(zhēng),提高了性能
分段 CAS 機(jī)制:
- 在發(fā)生競(jìng)爭(zhēng)時(shí),創(chuàng)建 Cell 數(shù)組用于將不同線程的操作離散(通過 hash 等算法映射)到不同的節(jié)點(diǎn)上
- 設(shè)置多個(gè)累加單元(會(huì)根據(jù)需要擴(kuò)容,最大為 CPU 核數(shù)),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后將結(jié)果匯總
- 在累加時(shí)操作的不同的 Cell 變量,因此減少了 CAS 重試失敗,從而提高性能
自動(dòng)分段遷移機(jī)制:某個(gè) Cell 的 value 執(zhí)行 CAS 失敗,就會(huì)自動(dòng)尋找另一個(gè) Cell 分段內(nèi)的 value 值進(jìn)行 CAS 操作
偽共享
Cell 為累加單元:數(shù)組訪問索引是通過 Thread 里的 threadLocalRandomProbe 域取模實(shí)現(xiàn)的,這個(gè)域是 ThreadLocalRandom 更新的
// Striped64.Cell
@sun.misc.Contended static final class Cell {volatile long value;Cell(long x) { value = x; }// 用 cas 方式進(jìn)行累加, prev 表示舊值, next 表示新值final boolean cas(long prev, long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}// 省略不重要代碼
}
Cell 是數(shù)組形式,在內(nèi)存中是連續(xù)存儲(chǔ)的,64 位系統(tǒng)中,一個(gè) Cell 為 24 字節(jié)(16 字節(jié)的對(duì)象頭和 8 字節(jié)的 value),每一個(gè) cache line 為 64 字節(jié),因此緩存行可以存下 2 個(gè)的 Cell 對(duì)象,當(dāng) Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],無論誰修改成功都會(huì)導(dǎo)致當(dāng)前緩存行失效,從而導(dǎo)致對(duì)方的數(shù)據(jù)失效,需要重新去主存獲取,影響效率
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-wQdE5bIW-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-偽共享1.png)]
@sun.misc.Contended:防止緩存行偽共享,在使用此注解的對(duì)象或字段的前后各增加 128 字節(jié)大小的 padding,使用 2 倍于大多數(shù)硬件緩存行讓 CPU 將對(duì)象預(yù)讀至緩存時(shí)占用不同的緩存行,這樣就不會(huì)造成對(duì)方緩存行的失效
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-1O1S8rxm-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-偽共享2.png)]
源碼解析
Striped64 類成員屬性:
// 表示當(dāng)前計(jì)算機(jī)CPU數(shù)量
static final int NCPU = Runtime.getRuntime().availableProcessors()
// 累加單元數(shù)組, 懶惰初始化
transient volatile Cell[] cells;
// 基礎(chǔ)值, 如果沒有競(jìng)爭(zhēng), 則用 cas 累加這個(gè)域,當(dāng) cells 擴(kuò)容時(shí),也會(huì)將數(shù)據(jù)寫到 base 中
transient volatile long base;
// 在 cells 初始化或擴(kuò)容時(shí)只能有一個(gè)線程執(zhí)行, 通過 CAS 更新 cellsBusy 置為 1 來實(shí)現(xiàn)一個(gè)鎖
transient volatile int cellsBusy;
工作流程:
-
cells 占用內(nèi)存是相對(duì)比較大的,是惰性加載的,在無競(jìng)爭(zhēng)或者其他線程正在初始化 cells 數(shù)組的情況下,直接更新 base 域
-
在第一次發(fā)生競(jìng)爭(zhēng)時(shí)(casBase 失敗)會(huì)創(chuàng)建一個(gè)大小為 2 的 cells 數(shù)組,將當(dāng)前累加的值包裝為 Cell 對(duì)象,放入映射的槽位上
-
分段累加的過程中,如果當(dāng)前線程對(duì)應(yīng)的 cells 槽位為空,就會(huì)新建 Cell 填充,如果出現(xiàn)競(jìng)爭(zhēng),就會(huì)重新計(jì)算線程對(duì)應(yīng)的槽位,繼續(xù)自旋嘗試修改
-
分段遷移后還出現(xiàn)競(jìng)爭(zhēng)就會(huì)擴(kuò)容 cells 數(shù)組長(zhǎng)度為原來的兩倍,然后 rehash,數(shù)組長(zhǎng)度總是 2 的 n 次冪,默認(rèn)最大為 CPU 核數(shù),但是可以超過,如果核數(shù)是 6 核,數(shù)組最長(zhǎng)是 8
方法分析:
-
LongAdder#add:累加方法
public void add(long x) {// as 為累加單元數(shù)組的引用,b 為基礎(chǔ)值,v 表示期望值// m 表示 cells 數(shù)組的長(zhǎng)度 - 1,a 表示當(dāng)前線程命中的 cell 單元格Cell[] as; long b, v; int m; Cell a;// cells 不為空說明 cells 已經(jīng)被初始化,線程發(fā)生了競(jìng)爭(zhēng),去更新對(duì)應(yīng)的 cell 槽位// 進(jìn)入 || 后的邏輯去更新 base 域,更新失敗表示發(fā)生競(jìng)爭(zhēng)進(jìn)入條件if ((as = cells) != null || !casBase(b = base, b + x)) {// uncontended 為 true 表示 cell 沒有競(jìng)爭(zhēng)boolean uncontended = true;// 條件一: true 說明 cells 未初始化,多線程寫 base 發(fā)生競(jìng)爭(zhēng)需要進(jìn)行初始化 cells 數(shù)組// fasle 說明 cells 已經(jīng)初始化,進(jìn)行下一個(gè)條件尋找自己的 cell 去累加// 條件二: getProbe() 獲取 hash 值,& m 的邏輯和 HashMap 的邏輯相同,保證散列的均勻性// true 說明當(dāng)前線程對(duì)應(yīng)下標(biāo)的 cell 為空,需要?jiǎng)?chuàng)建 cell// false 說明當(dāng)前線程對(duì)應(yīng)的 cell 不為空,進(jìn)行下一個(gè)條件【將 x 值累加到對(duì)應(yīng)的 cell 中】// 條件三: 有取反符號(hào),false 說明 cas 成功,直接返回,true 說明失敗,當(dāng)前線程對(duì)應(yīng)的 cell 有競(jìng)爭(zhēng)if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);// 【uncontended 在對(duì)應(yīng)的 cell 上累加失敗的時(shí)候才為 false,其余情況均為 true】} }
-
Striped64#longAccumulate:cell 數(shù)組創(chuàng)建
// x null false | true final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {int h;// 當(dāng)前線程還沒有對(duì)應(yīng)的 cell, 需要隨機(jī)生成一個(gè) hash 值用來將當(dāng)前線程綁定到 cellif ((h = getProbe()) == 0) {// 初始化 probe,獲取 hash 值ThreadLocalRandom.current(); h = getProbe(); // 默認(rèn)情況下 當(dāng)前線程肯定是寫入到了 cells[0] 位置,不把它當(dāng)做一次真正的競(jìng)爭(zhēng)wasUncontended = true;}// 表示【擴(kuò)容意向】,false 一定不會(huì)擴(kuò)容,true 可能會(huì)擴(kuò)容boolean collide = false; //自旋for (;;) {// as 表示cells引用,a 表示當(dāng)前線程命中的 cell,n 表示 cells 數(shù)組長(zhǎng)度,v 表示 期望值Cell[] as; Cell a; int n; long v;// 【CASE1】: 表示 cells 已經(jīng)初始化了,當(dāng)前線程應(yīng)該將數(shù)據(jù)寫入到對(duì)應(yīng)的 cell 中if ((as = cells) != null && (n = as.length) > 0) {// CASE1.1: true 表示當(dāng)前線程對(duì)應(yīng)的索引下標(biāo)的 Cell 為 null,需要?jiǎng)?chuàng)建 new Cellif ((a = as[(n - 1) & h]) == null) {// 判斷 cellsBusy 是否被鎖if (cellsBusy == 0) { // 創(chuàng)建 cell, 初始累加值為 xCell r = new Cell(x); // 加鎖if (cellsBusy == 0 && casCellsBusy()) {// 創(chuàng)建成功標(biāo)記,進(jìn)入【創(chuàng)建 cell 邏輯】boolean created = false; try {Cell[] rs; int m, j;// 把當(dāng)前 cells 數(shù)組賦值給 rs,并且不為 nullif ((rs = cells) != null &&(m = rs.length) > 0 &&// 再次判斷防止其它線程初始化過該位置,當(dāng)前線程再次初始化該位置會(huì)造成數(shù)據(jù)丟失// 因?yàn)檫@里是線程安全的判斷,進(jìn)行的邏輯不會(huì)被其他線程影響rs[j = (m - 1) & h] == null) {// 把新創(chuàng)建的 cell 填充至當(dāng)前位置rs[j] = r;created = true; // 表示創(chuàng)建完成}} finally {cellsBusy = 0; // 解鎖}if (created) // true 表示創(chuàng)建完成,可以推出循環(huán)了break;continue;}}collide = false;}// CASE1.2: 條件成立說明線程對(duì)應(yīng)的 cell 有競(jìng)爭(zhēng), 改變線程對(duì)應(yīng)的 cell 來重試 caselse if (!wasUncontended)wasUncontended = true;// CASE 1.3: 當(dāng)前線程 rehash 過,如果新命中的 cell 不為空,就嘗試?yán)奂?#xff0c;false 說明新命中也有競(jìng)爭(zhēng)else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))break;// CASE 1.4: cells 長(zhǎng)度已經(jīng)超過了最大長(zhǎng)度 CPU 內(nèi)核的數(shù)量或者已經(jīng)擴(kuò)容else if (n >= NCPU || cells != as)collide = false; // 擴(kuò)容意向改為false,【表示不能擴(kuò)容了】// CASE 1.5: 更改擴(kuò)容意向,如果 n >= NCPU,這里就永遠(yuǎn)不會(huì)執(zhí)行到,case1.4 永遠(yuǎn)先于 1.5 執(zhí)行else if (!collide)collide = true;// CASE 1.6: 【擴(kuò)容邏輯】,進(jìn)行加鎖else if (cellsBusy == 0 && casCellsBusy()) {try {// 線程安全的檢查,防止期間被其他線程擴(kuò)容了if (cells == as) { // 擴(kuò)容為以前的 2 倍Cell[] rs = new Cell[n << 1];// 遍歷移動(dòng)值for (int i = 0; i < n; ++i)rs[i] = as[i];// 把擴(kuò)容后的引用給 cellscells = rs;}} finally {cellsBusy = 0; // 解鎖}collide = false; // 擴(kuò)容意向改為 false,表示不擴(kuò)容了continue;}// 重置當(dāng)前線程 Hash 值,這就是【分段遷移機(jī)制】h = advanceProbe(h);}// 【CASE2】: 運(yùn)行到這說明 cells 還未初始化,as 為null// 判斷是否沒有加鎖,沒有加鎖就用 CAS 加鎖// 條件二判斷是否其它線程在當(dāng)前線程給 as 賦值之后修改了 cells,這里不是線程安全的判斷else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// 初始化標(biāo)志,開始 【初始化 cells 數(shù)組】boolean init = false;try { // 再次判斷 cells == as 防止其它線程已經(jīng)提前初始化了,當(dāng)前線程再次初始化導(dǎo)致丟失數(shù)據(jù)// 因?yàn)檫@里是【線程安全的,重新檢查,經(jīng)典 DCL】if (cells == as) {Cell[] rs = new Cell[2]; // 初始化數(shù)組大小為2rs[h & 1] = new Cell(x); // 填充線程對(duì)應(yīng)的cellcells = rs;init = true; // 初始化成功,標(biāo)記置為 true}} finally {cellsBusy = 0; // 解鎖啊}if (init)break; // 初始化成功直接跳出自旋}// 【CASE3】: 運(yùn)行到這說明其他線程在初始化 cells,當(dāng)前線程將值累加到 base,累加成功直接結(jié)束自旋else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break; } }
-
sum:獲取最終結(jié)果通過 sum 整合,保證最終一致性,不保證強(qiáng)一致性
public long sum() {Cell[] as = cells; Cell a;long sum = base;if (as != null) {// 遍歷 累加for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum; }
ABA
ABA 問題:當(dāng)進(jìn)行獲取主內(nèi)存值時(shí),該內(nèi)存值在寫入主內(nèi)存時(shí)已經(jīng)被修改了 N 次,但是最終又改成原來的值
其他線程先把 A 改成 B 又改回 A,主線程僅能判斷出共享變量的值與最初值 A 是否相同,不能感知到這種從 A 改為 B 又 改回 A 的情況,這時(shí) CAS 雖然成功,但是過程存在問題
-
構(gòu)造方法:
public AtomicStampedReference(V initialRef, int initialStamp)
:初始值和初始版本號(hào)
-
常用API:
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
:期望引用和期望版本號(hào)都一致才進(jìn)行 CAS 修改數(shù)據(jù)public void set(V newReference, int newStamp)
:設(shè)置值和版本號(hào)public V getReference()
:返回引用的值public int getStamp()
:返回當(dāng)前版本號(hào)
public static void main(String[] args) {AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);int startStamp = atomicReference.getStamp();new Thread(() ->{int stamp = atomicReference.getStamp();atomicReference.compareAndSet(100, 101, stamp, stamp + 1);stamp = atomicReference.getStamp();atomicReference.compareAndSet(101, 100, stamp, stamp + 1);},"t1").start();new Thread(() ->{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {System.out.println(atomicReference.getReference());//100System.out.println(Thread.currentThread().getName() + "線程修改失敗");}},"t2").start();
}
Unsafe
Unsafe 是 CAS 的核心類,由于 Java 無法直接訪問底層系統(tǒng),需要通過本地(Native)方法來訪問
Unsafe 類存在 sun.misc 包,其中所有方法都是 native 修飾的,都是直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)的任務(wù),基于該類可以直接操作特定的內(nèi)存數(shù)據(jù),其內(nèi)部方法操作類似 C 的指針
模擬實(shí)現(xiàn)原子整數(shù):
public static void main(String[] args) {MyAtomicInteger atomicInteger = new MyAtomicInteger(10);if (atomicInteger.compareAndSwap(20)) {System.out.println(atomicInteger.getValue());}
}class MyAtomicInteger {private static final Unsafe UNSAFE;private static final long VALUE_OFFSET;private volatile int value;static {try {//Unsafe unsafe = Unsafe.getUnsafe()這樣會(huì)報(bào)錯(cuò),需要反射獲取Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);UNSAFE = (Unsafe) theUnsafe.get(null);// 獲取 value 屬性的內(nèi)存地址,value 屬性指向該地址,直接設(shè)置該地址的值可以修改 value 的值VALUE_OFFSET = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();throw new RuntimeException();}}public MyAtomicInteger(int value) {this.value = value;}public int getValue() {return value;}public boolean compareAndSwap(int update) {while (true) {int prev = this.value;int next = update;// 當(dāng)前對(duì)象 內(nèi)存偏移量 期望值 更新值if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {System.out.println("CAS成功");return true;}}}
}
final
原理
public class TestFinal {final int a = 20;
}
字節(jié)碼:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20 // 將值直接放入棧中
7: putfield #2 // Field a:I
<-- 寫屏障
10: return
final 變量的賦值通過 putfield 指令來完成,在這條指令之后也會(huì)加入寫屏障,保證在其它線程讀到它的值時(shí)不會(huì)出現(xiàn)為 0 的情況
其他線程訪問 final 修飾的變量
- 復(fù)制一份放入棧中直接訪問,效率高
- 大于 short 最大值會(huì)將其復(fù)制到類的常量池,訪問時(shí)從常量池獲取
不可變
不可變:如果一個(gè)對(duì)象不能夠修改其內(nèi)部狀態(tài)(屬性),那么就是不可變對(duì)象
不可變對(duì)象線程安全的,不存在并發(fā)修改和可見性問題,是另一種避免競(jìng)爭(zhēng)的方式
String 類也是不可變的,該類和類中所有屬性都是 final 的
-
類用 final 修飾保證了該類中的方法不能被覆蓋,防止子類無意間破壞不可變性
-
無寫入方法(set)確保外部不能對(duì)內(nèi)部屬性進(jìn)行修改
-
屬性用 final 修飾保證了該屬性是只讀的,不能修改
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];//.... }
-
更改 String 類數(shù)據(jù)時(shí),會(huì)構(gòu)造新字符串對(duì)象,生成新的 char[] value,通過創(chuàng)建副本對(duì)象來避免共享的方式稱之為保護(hù)性拷貝
State
無狀態(tài):成員變量保存的數(shù)據(jù)也可以稱為狀態(tài)信息,無狀態(tài)就是沒有成員變量
Servlet 為了保證其線程安全,一般不為 Servlet 設(shè)置成員變量,這種沒有任何成員變量的類是線程安全的
Local
基本介紹
ThreadLocal 類用來提供線程內(nèi)部的局部變量,這種變量在多線程環(huán)境下訪問(通過 get 和 set 方法訪問)時(shí)能保證各個(gè)線程的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量,分配在堆內(nèi)的 TLAB 中
ThreadLocal 實(shí)例通常來說都是 private static
類型的,屬于一個(gè)線程的本地變量,用于關(guān)聯(lián)線程和線程上下文。每個(gè)線程都會(huì)在 ThreadLocal 中保存一份該線程獨(dú)有的數(shù)據(jù),所以是線程安全的
ThreadLocal 作用:
-
線程并發(fā):應(yīng)用在多線程并發(fā)的場(chǎng)景下
-
傳遞數(shù)據(jù):通過 ThreadLocal 實(shí)現(xiàn)在同一線程不同函數(shù)或組件中傳遞公共變量,減少傳遞復(fù)雜度
-
線程隔離:每個(gè)線程的變量都是獨(dú)立的,不會(huì)互相影響
對(duì)比 synchronized:
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步機(jī)制采用以時(shí)間換空間的方式,只提供了一份變量,讓不同的線程排隊(duì)訪問 | ThreadLocal 采用以空間換時(shí)間的方式,為每個(gè)線程都提供了一份變量的副本,從而實(shí)現(xiàn)同時(shí)訪問而相不干擾 |
側(cè)重點(diǎn) | 多個(gè)線程之間訪問資源的同步 | 多線程中讓每個(gè)線程之間的數(shù)據(jù)相互隔離 |
基本使用
常用方法
方法 | 描述 |
---|---|
ThreadLocal<>() | 創(chuàng)建 ThreadLocal 對(duì)象 |
protected T initialValue() | 返回當(dāng)前線程局部變量的初始值 |
public void set( T value) | 設(shè)置當(dāng)前線程綁定的局部變量 |
public T get() | 獲取當(dāng)前線程綁定的局部變量 |
public void remove() | 移除當(dāng)前線程綁定的局部變量 |
public class MyDemo {private static ThreadLocal<String> tl = new ThreadLocal<>();private String content;private String getContent() {// 獲取當(dāng)前線程綁定的變量return tl.get();}private void setContent(String content) {// 變量content綁定到當(dāng)前線程tl.set(content);}public static void main(String[] args) {MyDemo demo = new MyDemo();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 設(shè)置數(shù)據(jù)demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());}});thread.setName("線程" + i);thread.start();}}
}
應(yīng)用場(chǎng)景
ThreadLocal 適用于下面兩種場(chǎng)景:
- 每個(gè)線程需要有自己?jiǎn)为?dú)的實(shí)例
- 實(shí)例需要在多個(gè)方法中共享,但不希望被多線程共享
ThreadLocal 方案有兩個(gè)突出的優(yōu)勢(shì):
- 傳遞數(shù)據(jù):保存每個(gè)線程綁定的數(shù)據(jù),在需要的地方可以直接獲取,避免參數(shù)直接傳遞帶來的代碼耦合問題
- 線程隔離:各線程之間的數(shù)據(jù)相互隔離卻又具備并發(fā)性,避免同步方式帶來的性能損失
ThreadLocal 用于數(shù)據(jù)連接的事務(wù)管理:
public class JdbcUtils {// ThreadLocal對(duì)象,將connection綁定在當(dāng)前線程中private static final ThreadLocal<Connection> tl = new ThreadLocal();// c3p0 數(shù)據(jù)庫連接池對(duì)象屬性private static final ComboPooledDataSource ds = new ComboPooledDataSource();// 獲取連接public static Connection getConnection() throws SQLException {//取出當(dāng)前線程綁定的connection對(duì)象Connection conn = tl.get();if (conn == null) {//如果沒有,則從連接池中取出conn = ds.getConnection();//再將connection對(duì)象綁定到當(dāng)前線程中,非常重要的操作tl.set(conn);}return conn;}// ...
}
用 ThreadLocal 使 SimpleDateFormat 從獨(dú)享變量變成單個(gè)線程變量:
public class ThreadLocalDateUtil {private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};public static Date parse(String dateStr) throws ParseException {return threadLocal.get().parse(dateStr);}public static String format(Date date) {return threadLocal.get().format(date);}
}
實(shí)現(xiàn)原理
底層結(jié)構(gòu)
JDK8 以前:每個(gè) ThreadLocal 都創(chuàng)建一個(gè) Map,然后用線程作為 Map 的 key,要存儲(chǔ)的局部變量作為 Map 的 value,達(dá)到各個(gè)線程的局部變量隔離的效果。這種結(jié)構(gòu)會(huì)造成 Map 結(jié)構(gòu)過大和內(nèi)存泄露,因?yàn)?Thread 停止后無法通過 key 刪除對(duì)應(yīng)的數(shù)據(jù)
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-Tbx1aoOA-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal數(shù)據(jù)結(jié)構(gòu)JDK8前.png)]
JDK8 以后:每個(gè) Thread 維護(hù)一個(gè) ThreadLocalMap,這個(gè) Map 的 key 是 ThreadLocal 實(shí)例本身,value 是真正要存儲(chǔ)的值
- 每個(gè) Thread 線程內(nèi)部都有一個(gè) Map (ThreadLocalMap)
- Map 里面存儲(chǔ) ThreadLocal 對(duì)象(key)和線程的私有變量(value)
- Thread 內(nèi)部的 Map 是由 ThreadLocal 維護(hù)的,由 ThreadLocal 負(fù)責(zé)向 map 獲取和設(shè)置線程的變量值
- 對(duì)于不同的線程,每次獲取副本值時(shí),別的線程并不能獲取到當(dāng)前線程的副本值,形成副本的隔離,互不干擾
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-zzLTCp1t-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal數(shù)據(jù)結(jié)構(gòu)JDK8后.png)]
JDK8 前后對(duì)比:
- 每個(gè) Map 存儲(chǔ)的 Entry 數(shù)量會(huì)變少,因?yàn)橹暗拇鎯?chǔ)數(shù)量由 Thread 的數(shù)量決定,現(xiàn)在由 ThreadLocal 的數(shù)量決定,在實(shí)際編程當(dāng)中,往往 ThreadLocal 的數(shù)量要少于 Thread 的數(shù)量
- 當(dāng) Thread 銷毀之后,對(duì)應(yīng)的 ThreadLocalMap 也會(huì)隨之銷毀,能減少內(nèi)存的使用,防止內(nèi)存泄露
成員變量
-
Thread 類的相關(guān)屬性:每一個(gè)線程持有一個(gè) ThreadLocalMap 對(duì)象,存放由 ThreadLocal 和數(shù)據(jù)組成的 Entry 鍵值對(duì)
ThreadLocal.ThreadLocalMap threadLocals = null
-
計(jì)算 ThreadLocal 對(duì)象的哈希值:
private final int threadLocalHashCode = nextHashCode()
使用
threadLocalHashCode & (table.length - 1)
計(jì)算當(dāng)前 entry 需要存放的位置 -
每創(chuàng)建一個(gè) ThreadLocal 對(duì)象就會(huì)使用 nextHashCode 分配一個(gè) hash 值給這個(gè)對(duì)象:
private static AtomicInteger nextHashCode = new AtomicInteger()
-
斐波那契數(shù)也叫黃金分割數(shù),hash 的增量就是這個(gè)數(shù)字,帶來的好處是 hash 分布非常均勻:
private static final int HASH_INCREMENT = 0x61c88647
成員方法
方法都是線程安全的,因?yàn)?ThreadLocal 屬于一個(gè)線程的,ThreadLocal 中的方法,邏輯都是獲取當(dāng)前線程維護(hù)的 ThreadLocalMap 對(duì)象,然后進(jìn)行數(shù)據(jù)的增刪改查,沒有指定初始值的 threadlcoal 對(duì)象默認(rèn)賦值為 null
-
initialValue():返回該線程局部變量的初始值
- 延遲調(diào)用的方法,在執(zhí)行 get 方法時(shí)才執(zhí)行
- 該方法缺省(默認(rèn))實(shí)現(xiàn)直接返回一個(gè) null
- 如果想要一個(gè)初始值,可以重寫此方法, 該方法是一個(gè)
protected
的方法,為了讓子類覆蓋而設(shè)計(jì)的
protected T initialValue() {return null; }
-
nextHashCode():計(jì)算哈希值,ThreadLocal 的散列方式稱之為斐波那契散列,每次獲取哈希值都會(huì)加上 HASH_INCREMENT,這樣做可以盡量避免 hash 沖突,讓哈希值能均勻的分布在 2 的 n 次方的數(shù)組中
private static int nextHashCode() {// 哈希值自增一個(gè) HASH_INCREMENT 數(shù)值return nextHashCode.getAndAdd(HASH_INCREMENT); }
-
set():修改當(dāng)前線程與當(dāng)前 threadlocal 對(duì)象相關(guān)聯(lián)的線程局部變量
public void set(T value) {// 獲取當(dāng)前線程對(duì)象Thread t = Thread.currentThread();// 獲取此線程對(duì)象中維護(hù)的 ThreadLocalMap 對(duì)象ThreadLocalMap map = getMap(t);// 判斷 map 是否存在if (map != null)// 調(diào)用 threadLocalMap.set 方法進(jìn)行重寫或者添加map.set(this, value);else// map 為空,調(diào)用 createMap 進(jìn)行 ThreadLocalMap 對(duì)象的初始化。參數(shù)1是當(dāng)前線程,參數(shù)2是局部變量createMap(t, value); }
// 獲取當(dāng)前線程 Thread 對(duì)應(yīng)維護(hù)的 ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; } // 創(chuàng)建當(dāng)前線程Thread對(duì)應(yīng)維護(hù)的ThreadLocalMap void createMap(Thread t, T firstValue) {// 【這里的 this 是調(diào)用此方法的 threadLocal】,創(chuàng)建一個(gè)新的 Map 并設(shè)置第一個(gè)數(shù)據(jù)t.threadLocals = new ThreadLocalMap(this, firstValue); }
-
get():獲取當(dāng)前線程與當(dāng)前 ThreadLocal 對(duì)象相關(guān)聯(lián)的線程局部變量
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果此map存在if (map != null) {// 以當(dāng)前的 ThreadLocal 為 key,調(diào)用 getEntry 獲取對(duì)應(yīng)的存儲(chǔ)實(shí)體 eThreadLocalMap.Entry e = map.getEntry(this);// 對(duì) e 進(jìn)行判空 if (e != null) {// 獲取存儲(chǔ)實(shí)體 e 對(duì)應(yīng)的 value值T result = (T)e.value;return result;}}/*有兩種情況有執(zhí)行當(dāng)前代碼第一種情況: map 不存在,表示此線程沒有維護(hù)的 ThreadLocalMap 對(duì)象第二種情況: map 存在, 但是【沒有與當(dāng)前 ThreadLocal 關(guān)聯(lián)的 entry】,就會(huì)設(shè)置為默認(rèn)值 */// 初始化當(dāng)前線程與當(dāng)前 threadLocal 對(duì)象相關(guān)聯(lián)的 valuereturn setInitialValue(); }
private T setInitialValue() {// 調(diào)用initialValue獲取初始化的值,此方法可以被子類重寫, 如果不重寫默認(rèn)返回 nullT value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 判斷 map 是否初始化過if (map != null)// 存在則調(diào)用 map.set 設(shè)置此實(shí)體 entry,value 是默認(rèn)的值map.set(this, value);else// 調(diào)用 createMap 進(jìn)行 ThreadLocalMap 對(duì)象的初始化中createMap(t, value);// 返回線程與當(dāng)前 threadLocal 關(guān)聯(lián)的局部變量return value; }
-
remove():移除當(dāng)前線程與當(dāng)前 threadLocal 對(duì)象相關(guān)聯(lián)的線程局部變量
public void remove() {// 獲取當(dāng)前線程對(duì)象中維護(hù)的 ThreadLocalMap 對(duì)象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// map 存在則調(diào)用 map.remove,this時(shí)當(dāng)前ThreadLocal,以this為key刪除對(duì)應(yīng)的實(shí)體m.remove(this); }
LocalMap
成員屬性
ThreadLocalMap 是 ThreadLocal 的內(nèi)部類,沒有實(shí)現(xiàn) Map 接口,用獨(dú)立的方式實(shí)現(xiàn)了 Map 的功能,其內(nèi)部 Entry 也是獨(dú)立實(shí)現(xiàn)
// 初始化當(dāng)前 map 內(nèi)部散列表數(shù)組的初始長(zhǎng)度 16
private static final int INITIAL_CAPACITY = 16;// 存放數(shù)據(jù)的table,數(shù)組長(zhǎng)度必須是2的整次冪。
private Entry[] table;// 數(shù)組里面 entrys 的個(gè)數(shù),可以用于判斷 table 當(dāng)前使用量是否超過閾值
private int size = 0;// 進(jìn)行擴(kuò)容的閾值,表使用量大于它的時(shí)候進(jìn)行擴(kuò)容。
private int threshold;
存儲(chǔ)結(jié)構(gòu) Entry:
- Entry 繼承 WeakReference,key 是弱引用,目的是將 ThreadLocal 對(duì)象的生命周期和線程生命周期解綁
- Entry 限制只能用 ThreadLocal 作為 key,key 為 null (entry.get() == null) 意味著 key 不再被引用,entry 也可以從 table 中清除
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {// this.referent = referent = key;super(k);value = v;}
}
構(gòu)造方法:延遲初始化的,線程第一次存儲(chǔ) threadLocal - value 時(shí)才會(huì)創(chuàng)建 threadLocalMap 對(duì)象
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化table,創(chuàng)建一個(gè)長(zhǎng)度為16的Entry數(shù)組table = new Entry[INITIAL_CAPACITY];// 【尋址算法】計(jì)算索引int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 創(chuàng)建 entry 對(duì)象,存放到指定位置的 slot 中table[i] = new Entry(firstKey, firstValue);// 數(shù)據(jù)總量是 1size = 1;// 將閾值設(shè)置為 (當(dāng)前數(shù)組長(zhǎng)度 * 2)/ 3。setThreshold(INITIAL_CAPACITY);
}
成員方法
-
set():添加數(shù)據(jù),ThreadLocalMap 使用線性探測(cè)法來解決哈希沖突
-
該方法會(huì)一直探測(cè)下一個(gè)地址,直到有空的地址后插入,若插入后 Map 數(shù)量超過閾值,數(shù)組會(huì)擴(kuò)容為原來的 2 倍
假設(shè)當(dāng)前 table 長(zhǎng)度為16,計(jì)算出來 key 的 hash 值為 14,如果 table[14] 上已經(jīng)有值,并且其 key 與當(dāng)前 key 不一致,那么就發(fā)生了 hash 沖突,這個(gè)時(shí)候?qū)?14 加 1 得到 15,取 table[15] 進(jìn)行判斷,如果還是沖突會(huì)回到 0,取 table[0],以此類推,直到可以插入,可以把 Entry[] table 看成一個(gè)環(huán)形數(shù)組
-
線性探測(cè)法會(huì)出現(xiàn)堆積問題,可以采取平方探測(cè)法解決
-
在探測(cè)過程中 ThreadLocal 會(huì)復(fù)用 key 為 null 的臟 Entry 對(duì)象,并進(jìn)行垃圾清理,防止出現(xiàn)內(nèi)存泄漏
private void set(ThreadLocal<?> key, Object value) {// 獲取散列表ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 哈希尋址int i = key.threadLocalHashCode & (len-1);// 使用線性探測(cè)法向后查找元素,碰到 entry 為空時(shí)停止探測(cè)for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 獲取當(dāng)前元素 keyThreadLocal<?> k = e.get();// ThreadLocal 對(duì)應(yīng)的 key 存在,【直接覆蓋之前的值】if (k == key) {e.value = value;return;}// 【這兩個(gè)條件誰先成立不一定,所以 replaceStaleEntry 中還需要判斷 k == key 的情況】// key 為 null,但是值不為 null,說明之前的 ThreadLocal 對(duì)象已經(jīng)被回收了,當(dāng)前是【過期數(shù)據(jù)】if (k == null) {// 【碰到一個(gè)過期的 slot,當(dāng)前數(shù)據(jù)復(fù)用該槽位,替換過期數(shù)據(jù)】// 這個(gè)方法還進(jìn)行了垃圾清理動(dòng)作,防止內(nèi)存泄漏replaceStaleEntry(key, value, i);return;}}// 邏輯到這說明碰到 slot == null 的位置,則在空元素的位置創(chuàng)建一個(gè)新的 Entrytab[i] = new Entry(key, value);// 數(shù)量 + 1int sz = ++size;// 【做一次啟發(fā)式清理】,如果沒有清除任何 entry 并且【當(dāng)前使用量達(dá)到了負(fù)載因子所定義,那么進(jìn)行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)// 擴(kuò)容rehash(); }
// 獲取【環(huán)形數(shù)組】的下一個(gè)索引 private static int nextIndex(int i, int len) {// 索引越界后從 0 開始繼續(xù)獲取return ((i + 1 < len) ? i + 1 : 0); }
// 在指定位置插入指定的數(shù)據(jù) private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {// 獲取散列表Entry[] tab = table;int len = tab.length;Entry e;// 探測(cè)式清理的開始下標(biāo),默認(rèn)從當(dāng)前 staleSlot 開始int slotToExpunge = staleSlot;// 以當(dāng)前 staleSlot 開始【向前迭代查找】,找到索引靠前過期數(shù)據(jù),找到以后替換 slotToExpunge 值// 【保證在一個(gè)區(qū)間段內(nèi),從最前面的過期數(shù)據(jù)開始清理】for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 以 staleSlot 【向后去查找】,直到碰到 null 為止,還是線性探測(cè)for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {// 獲取當(dāng)前節(jié)點(diǎn)的 keyThreadLocal<?> k = e.get();// 條件成立說明是【替換邏輯】if (k == key) {e.value = value;// 因?yàn)楸緛硪?staleSlot 索引處插入該數(shù)據(jù),現(xiàn)在找到了i索引處的key與數(shù)據(jù)一致// 但是 i 位置距離正確的位置更遠(yuǎn),因?yàn)槭窍蚝蟛檎?#xff0c;所以還是要在 staleSlot 位置插入當(dāng)前 entry// 然后將 table[staleSlot] 這個(gè)過期數(shù)據(jù)放到當(dāng)前循環(huán)到的 table[i] 這個(gè)位置,tab[i] = tab[staleSlot];tab[staleSlot] = e;// 條件成立說明向前查找過期數(shù)據(jù)并未找到過期的 entry,但 staleSlot 位置已經(jīng)不是過期數(shù)據(jù)了,i 位置才是if (slotToExpunge == staleSlot)slotToExpunge = i;// 【清理過期數(shù)據(jù),expungeStaleEntry 探測(cè)式清理,cleanSomeSlots 啟發(fā)式清理】cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 條件成立說明當(dāng)前遍歷的 entry 是一個(gè)過期數(shù)據(jù),并且該位置前面也沒有過期數(shù)據(jù)if (k == null && slotToExpunge == staleSlot)// 探測(cè)式清理過期數(shù)據(jù)的開始下標(biāo)修改為當(dāng)前循環(huán)的 index,因?yàn)?staleSlot 會(huì)放入要添加的數(shù)據(jù)slotToExpunge = i;}// 向后查找過程中并未發(fā)現(xiàn) k == key 的 entry,說明當(dāng)前是一個(gè)【取代過期數(shù)據(jù)邏輯】// 刪除原有的數(shù)據(jù)引用,防止內(nèi)存泄露tab[staleSlot].value = null;// staleSlot 位置添加數(shù)據(jù),【上面的所有邏輯都不會(huì)更改 staleSlot 的值】tab[staleSlot] = new Entry(key, value);// 條件成立說明除了 staleSlot 以外,還發(fā)現(xiàn)其它的過期 slot,所以要【開啟清理數(shù)據(jù)的邏輯】if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-xSnZ9xSE-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry流程.png)]
private static int prevIndex(int i, int len) {// 形成一個(gè)環(huán)繞式的訪問,頭索引越界后置為尾索引return ((i - 1 >= 0) ? i - 1 : len - 1); }
-
-
getEntry():ThreadLocal 的 get 方法以當(dāng)前的 ThreadLocal 為 key,調(diào)用 getEntry 獲取對(duì)應(yīng)的存儲(chǔ)實(shí)體 e
private Entry getEntry(ThreadLocal<?> key) {// 哈希尋址int i = key.threadLocalHashCode & (table.length - 1);// 訪問散列表中指定指定位置的 slot Entry e = table[i];// 條件成立,說明 slot 有值并且 key 就是要尋找的 key,直接返回if (e != null && e.get() == key)return e;else// 進(jìn)行線性探測(cè)return getEntryAfterMiss(key, i, e); } // 線性探測(cè)尋址 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {// 獲取散列表Entry[] tab = table;int len = tab.length;// 開始遍歷,碰到 slot == null 的情況,搜索結(jié)束while (e != null) {// 獲取當(dāng)前 slot 中 entry 對(duì)象的 keyThreadLocal<?> k = e.get();// 條件成立說明找到了,直接返回if (k == key)return e;if (k == null)// 過期數(shù)據(jù),【探測(cè)式過期數(shù)據(jù)回收】expungeStaleEntry(i);else// 更新 index 繼續(xù)向后走i = nextIndex(i, len);// 獲取下一個(gè)槽位中的 entrye = tab[i];}// 說明當(dāng)前區(qū)段沒有找到相應(yīng)數(shù)據(jù)// 【因?yàn)榇娣艛?shù)據(jù)是線性的向后尋找槽位,都是緊挨著的,不可能越過一個(gè) 空槽位 在后面放】,可以減少遍歷的次數(shù)return null; }
-
rehash():觸發(fā)一次全量清理,如果數(shù)組長(zhǎng)度大于等于長(zhǎng)度的
2/3 * 3/4 = 1/2
,則進(jìn)行 resizeprivate void rehash() {// 清楚當(dāng)前散列表內(nèi)的【所有】過期的數(shù)據(jù)expungeStaleEntries();// threshold = len * 2 / 3,就是 2/3 * (1 - 1/4)if (size >= threshold - threshold / 4)resize(); }
private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;// 【遍歷所有的槽位,清理過期數(shù)據(jù)】for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);} }
Entry 數(shù)組為擴(kuò)容為原來的 2 倍 ,重新計(jì)算 key 的散列值,如果遇到 key 為 null 的情況,會(huì)將其 value 也置為 null,幫助 GC
private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新數(shù)組的長(zhǎng)度是老數(shù)組的二倍int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];// 統(tǒng)計(jì)新table中的entry數(shù)量int count = 0;// 遍歷老表,進(jìn)行【數(shù)據(jù)遷移】for (int j = 0; j < oldLen; ++j) {// 訪問老表的指定位置的 entryEntry e = oldTab[j];// 條件成立說明老表中該位置有數(shù)據(jù),可能是過期數(shù)據(jù)也可能不是if (e != null) {ThreadLocal<?> k = e.get();// 過期數(shù)據(jù)if (k == null) {e.value = null; // Help the GC} else {// 非過期數(shù)據(jù),在新表中進(jìn)行哈希尋址int h = k.threadLocalHashCode & (newLen - 1);// 【線程探測(cè)】while (newTab[h] != null)h = nextIndex(h, newLen);// 將數(shù)據(jù)存放到新表合適的 slot 中newTab[h] = e;count++;}}}// 設(shè)置下一次觸發(fā)擴(kuò)容的指標(biāo):threshold = len * 2 / 3;setThreshold(newLen);size = count;// 將擴(kuò)容后的新表賦值給 threadLocalMap 內(nèi)部散列表數(shù)組引用table = newTab; }
-
remove():刪除 Entry
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 哈希尋址int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 找到了對(duì)應(yīng)的 keyif (e.get() == key) {// 設(shè)置 key 為 nulle.clear();// 探測(cè)式清理expungeStaleEntry(i);return;}} }
清理方法
-
探測(cè)式清理:沿著開始位置向后探測(cè)清理過期數(shù)據(jù),沿途中碰到未過期數(shù)據(jù)則將此數(shù)據(jù) rehash 在 table 數(shù)組中的定位,重定位后的元素理論上更接近
i = entry.key & (table.length - 1)
,讓數(shù)據(jù)的排列更緊湊,會(huì)優(yōu)化整個(gè)散列表查詢性能// table[staleSlot] 是一個(gè)過期數(shù)據(jù),以這個(gè)位置開始繼續(xù)向后查找過期數(shù)據(jù) private int expungeStaleEntry(int staleSlot) {// 獲取散列表和數(shù)組長(zhǎng)度Entry[] tab = table;int len = tab.length;// help gc,先把當(dāng)前過期的 entry 置空,在取消對(duì) entry 的引用tab[staleSlot].value = null;tab[staleSlot] = null;// 數(shù)量-1size--;Entry e;int i;// 從 staleSlot 開始向后遍歷,直到碰到 slot == null 結(jié)束,【區(qū)間內(nèi)清理過期數(shù)據(jù)】for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 當(dāng)前 entry 是過期數(shù)據(jù)if (k == null) {// help gce.value = null;tab[i] = null;size--;} else {// 當(dāng)前 entry 不是過期數(shù)據(jù)的邏輯,【rehash】// 重新計(jì)算當(dāng)前 entry 對(duì)應(yīng)的 indexint h = k.threadLocalHashCode & (len - 1);// 條件成立說明當(dāng)前 entry 存儲(chǔ)時(shí)發(fā)生過 hash 沖突,向后偏移過了if (h != i) {// 當(dāng)前位置置空tab[i] = null;// 以正確位置 h 開始,向后查找第一個(gè)可以存放 entry 的位置while (tab[h] != null)h = nextIndex(h, len);// 將當(dāng)前元素放入到【距離正確位置更近的位置,有可能就是正確位置】tab[h] = e;}}}// 返回 slot = null 的槽位索引,圖例是 7,這個(gè)索引代表【索引前面的區(qū)間已經(jīng)清理完成垃圾了】return i; }
-
啟發(fā)式清理:向后循環(huán)掃描過期數(shù)據(jù),發(fā)現(xiàn)過期數(shù)據(jù)調(diào)用探測(cè)式清理方法,如果連續(xù)幾次的循環(huán)都沒有發(fā)現(xiàn)過期數(shù)據(jù),就停止掃描
// i 表示啟發(fā)式清理工作開始位置,一般是空 slot,n 一般傳遞的是 table.length private boolean cleanSomeSlots(int i, int n) {// 表示啟發(fā)式清理工作是否清除了過期數(shù)據(jù)boolean removed = false;// 獲取當(dāng)前 map 的散列表引用Entry[] tab = table;int len = tab.length;do {// 獲取下一個(gè)索引,因?yàn)樘綔y(cè)式返回的 slot 為 nulli = nextIndex(i, len);Entry e = tab[i];// 條件成立說明是過期的數(shù)據(jù),key 被 gc 了if (e != null && e.get() == null) {// 【發(fā)現(xiàn)過期數(shù)據(jù)重置 n 為數(shù)組的長(zhǎng)度】n = len;// 表示清理過過期數(shù)據(jù)removed = true;// 以當(dāng)前過期的 slot 為開始節(jié)點(diǎn) 做一次探測(cè)式清理工作i = expungeStaleEntry(i);}// 假設(shè) table 長(zhǎng)度為 16// 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0// 連續(xù)經(jīng)過這么多次循環(huán)【沒有掃描到過期數(shù)據(jù)】,就停止循環(huán),掃描到空 slot 不算,因?yàn)椴皇沁^期數(shù)據(jù)} while ((n >>>= 1) != 0);// 返回清除標(biāo)記return removed; }
參考視頻:https://space.bilibili.com/457326371/
內(nèi)存泄漏
Memory leak:內(nèi)存泄漏是指程序中動(dòng)態(tài)分配的堆內(nèi)存由于某種原因未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果,內(nèi)存泄漏的堆積終將導(dǎo)致內(nèi)存溢出
-
如果 key 使用強(qiáng)引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 強(qiáng)引用了 threadLocal,造成 threadLocal 無法被回收,無法完全避免內(nèi)存泄漏
-
如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此時(shí) Entry 中的 key = null。但沒有手動(dòng)刪除這個(gè) Entry 或者 CurrentThread 依然運(yùn)行,依然存在強(qiáng)引用鏈,value 不會(huì)被回收,而這塊 value 永遠(yuǎn)不會(huì)被訪問到,也會(huì)導(dǎo)致 value 內(nèi)存泄漏
-
兩個(gè)主要原因:
- 沒有手動(dòng)刪除這個(gè) Entry
- CurrentThread 依然運(yùn)行
根本原因:ThreadLocalMap 是 Thread的一個(gè)屬性,生命周期跟 Thread 一樣長(zhǎng),如果沒有手動(dòng)刪除對(duì)應(yīng) Entry 就會(huì)導(dǎo)致內(nèi)存泄漏
解決方法:使用完 ThreadLocal 中存儲(chǔ)的內(nèi)容后將它 remove 掉就可以
ThreadLocal 內(nèi)部解決方法:在 ThreadLocalMap 中的 set/getEntry 方法中,通過線性探測(cè)法對(duì) key 進(jìn)行判斷,如果 key 為 null(ThreadLocal 為 null)會(huì)對(duì) Entry 進(jìn)行垃圾回收。所以使用弱引用比強(qiáng)引用多一層保障,就算不調(diào)用 remove,也有機(jī)會(huì)進(jìn)行 GC
變量傳遞
基本使用
父子線程:創(chuàng)建子線程的線程是父線程,比如實(shí)例中的 main 線程就是父線程
ThreadLocal 中存儲(chǔ)的是線程的局部變量,如果想實(shí)現(xiàn)線程間局部變量傳遞可以使用 InheritableThreadLocal 類
public static void main(String[] args) {ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();threadLocal.set("父線程設(shè)置的值");new Thread(() -> System.out.println("子線程輸出:" + threadLocal.get())).start();
}
// 子線程輸出:父線程設(shè)置的值
實(shí)現(xiàn)原理
InheritableThreadLocal 源碼:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}
實(shí)現(xiàn)父子線程間的局部變量共享需要追溯到 Thread 對(duì)象的構(gòu)造方法:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,// 該參數(shù)默認(rèn)是 trueboolean inheritThreadLocals) {// ...Thread parent = currentThread();// 判斷父線程(創(chuàng)建子線程的線程)的 inheritableThreadLocals 屬性不為 nullif (inheritThreadLocals && parent.inheritableThreadLocals != null) {// 復(fù)制父線程的 inheritableThreadLocals 屬性,實(shí)現(xiàn)父子線程局部變量共享this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }// ..
}
// 【本質(zhì)上還是創(chuàng)建 ThreadLocalMap,只是把父類中的可繼承數(shù)據(jù)設(shè)置進(jìn)去了】
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {// 獲取父線程的哈希表Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];// 【逐個(gè)復(fù)制父線程 ThreadLocalMap 中的數(shù)據(jù)】for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// 調(diào)用的是 InheritableThreadLocal#childValue(T parentValue)Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);// 線性探測(cè)while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}
參考文章:https://blog.csdn.net/feichitianxia/article/details/110495764