JVM學習筆記——記憶體模型篇
JVM學習筆記——記憶體模型篇
在本系列內容中我們會對JVM做一個系統的學習,本片將會介紹JVM的記憶體模型部分
我們會分為以下幾部分進行介紹:
- 記憶體模型
- 樂觀鎖與悲觀鎖
- synchronized優化
記憶體模型
這一小節我們來詳細介紹一下記憶體模型和記憶體模型的三個特性
記憶體模型簡介
首先我們來簡單介紹一下記憶體模型:
- 記憶體模型,全稱Java Memory Model,也就是我們常說的JMM
- JMM中定義了一套在多執行緒讀寫共享數據時,對數據的可見性,有序性和原子性的規則和保障
記憶體模型之原子性
我們將在下面仔細介紹原子性的特點
原子性介紹
我們首先介紹一下原子性:
- 原子性是指將一系列操作規劃為一個操作,全稱不可分離進行
原子性的注意點:
- 我們在單執行緒下不會出現原子性的問題
- 但在多執行緒下,每條語句的實際底層操作不止一步,可能就會導致操作錯誤
原子性問題
我們給出一個簡單的例子來解釋原子性:
package cn.itcast.jvm.t4.avo;
// 在下述操作中,我們分別創造兩個執行緒,分別執行i++和i--50000次,按正常邏輯來說結果應該為0
public class Demo4_1 {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
但我們多次運行的結果如下:
// 每次結果均不相同
302
-986
0
原子性分析
首先我們分別給出i++和i–的底層操作:
// i++
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
iadd // 加法
putstatic i // 將修改後的值存入靜態變數i
// i--
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
isub // 減法
putstatic i // 將修改後的值存入靜態變數i
我們的原子性分為兩種情況:
- 單執行緒情況下:我們的順序肯定是按照正常順序來執行
- 多執行緒情況下:我們i++的操作按順序執行,i–的操作按順序執行,但兩者操作可能會交替進行
首先我們給出單執行緒情況下底層程式碼:
// 單執行緒
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=1
iconst_1 // 執行緒1-準備常量1
isub // 執行緒1-自減 執行緒內i=0
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=0
然後我們分別給出多執行緒情況下多種結果的底層程式碼:
// 多執行緒
// 負數
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1
// 正數
// 假設i的初始值為0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
原子性實現
那麼我們該如何實現多執行緒的原子性:
- 使用synchronized(同步關鍵字)
我們這裡給出synchronized的使用方式:
synchronized( 對象 ) {
// 要作為原子操作程式碼
}
我們如果要實現之前的程式碼,我們可以將程式碼修改為:
package cn.itcast.jvm.t4.avo;
public class Demo4_1 {
// 這裡的i應該被多執行緒共用,設為靜態變數
static int i = 0;
// 這裡是Obj對象,我們設置它為鎖,注意兩個執行緒中的synchronized所對應的鎖應該是同一個對象(鎖)
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 採用synchronized設置鎖實現原子性,這樣i++操作就會完整進行
Thread t1 = new Thread(() -> {
synchronized (obj) {
for (int j = 0; j < 50000; j++) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
// 採用synchronized設置鎖實現原子性,這樣i--操作就會完整進行
synchronized (obj) {
for (int j = 0; j < 50000; j++) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 我們的輸出結果自然是0了~
System.out.println(i);
}
}
記憶體模型之可見性
我們將在下面仔細介紹可見性的特點
可見性介紹
首先我們簡單介紹一下可見性的定義:
- 我們需要保證,在多個執行緒中,對同一變數的修改需要被其他執行緒所知道並且可以調用
可見性的注意點:
- 我們的程式往往具有自動優化,對於多次取同一值的數據可能會封裝在自己的程式中而不是在源程式讀取,這就會導致可見性失效
可見性問題
我們同樣給出一段程式碼作為可見性的案例:
package cn.itcast.jvm.t4.avo;
public class Demo4_2 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
}
我們的運行結果如下:
// 我們上述程式碼希望:程式在執行1s後停止運行,但我們的程式卻一直運行不會停止
...
可見性分析
首先我們回顧開頭的注意點:
- 程式具有自身很多的優化步驟,可能哪一步就會導致我們的程式出錯
我們來簡單分析:
- 初始狀態, t 執行緒剛開始從主記憶體讀取了 run 的值到工作記憶體。
- 執行緒要頻繁從主記憶體中讀取 run 的值,JIT 編譯器會將 run 的值快取至自己工作記憶體中的高速快取中,減少對主存中 run 的訪問
- 1 秒之後,main 執行緒修改了 run 的值,並同步至主存,而 t 是從自己工作記憶體中的高速快取中讀取這個變數的值,結果永遠是舊值
可見性實現
我們的可見性經常通過一種修飾詞來實現:
- volatile(易變關鍵字)
- 它可以用來修飾成員變數和靜態成員變數
- 他可以避免執行緒從自己的工作快取中查找變數的值,必須到主存中獲取它的值,執行緒操作 volatile 變數都是直接操作主存
同時我們給出另一種方法:
- synchronized 語句塊
- synchronized既可以保證程式碼塊的原子性,也同時保證程式碼塊內變數的可見性
- 但缺點是synchronized是屬於重量級操作,性能相對更低
我們如果修改之前程式碼,就可以採用volatile修改:
package cn.itcast.jvm.t4.avo;
public class Demo4_2 {
static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
Thread.sleep(1000);
run = false; // 執行緒t不會如預想的停下來
}
}
記憶體模型之有序性
我們將在下面仔細介紹有序性的特點
有序性介紹
首先我們簡單介紹一下有序性的定義:
- 有序性就是指我們底層程式碼實現的具體順序,在正常情況下是按正常順序執行
有序性的注意點:
- 同樣底層也會進行部分優化,對於有序性的優化常常被稱為指令重排,是指在不影響操作的前提下進行語句的優化調整
有序性問題
我們同樣給出一段程式碼:
int num = 0;
boolean ready = false;
// 執行緒1 執行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 執行緒2 執行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
我們下面會給出其所有情況:
// 具體分為四種情況,前三種屬於正常的多執行緒無鎖導致的情況
// 情況1:執行緒1 先執行,這時 ready = false,所以進入 else 分支結果為 1
// 情況2:執行緒2 先執行 num = 2,但沒來得及執行 ready = true,執行緒1 執行,還是進入 else 分支,結果為1
// 情況3:執行緒2 執行到 ready = true,執行緒1 執行,這回進入 if 分支,結果為 4(因為 num 已經執行過了)
// 但是第四種!卻是因為程式碼重排所導致的情況:
有序性分析
首先我們在重新介紹一下指令重排:
- JIT 編譯器在運行時的一些優化,這個現象需要通過大量測試才能復現
我們可以給出結果為0的執行順序:
執行緒2:ready = true;(由於操作更加簡單,導致JIT將它放在前面編譯)
執行緒1:if判斷 true
執行緒1:r.r1 = num + num;(此時num為0),結果r1=0
JVM 會在不影響正確性的前提下,可以調整語句的執行順序:
// 下面是模擬情況:
static int i;
static int j;
// 在某個執行緒內執行如下賦值操作
// i為較為耗時的操作,j為簡單操作
i = ...;
j = ...;
// 底層程式碼會認為i和j的賦值操作毫無關係,他們誰先執行都可以,所以會優先執行簡單的操作
// 所以我們的程式碼可能變為:
static int i;
static int j;
j = ...;
i = ...;
有序性實現
我們的可見性經常通過一種修飾詞來實現:
- volatile 修飾的變數,可以禁用指令重排
所以我們的程式碼經過修改後可以改造為以下程式碼:
int num = 0;
boolean volatile ready = false;
// 執行緒1 執行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 執行緒2 執行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
happens-before
我們在最後插入一個簡單的內容happens-before :
- 規定了哪些寫操作對其它執行緒的讀操作可見,它是可見性與有序性的一套規則總結
我們來簡單介紹一些:
- 執行緒 start 前對變數的寫,對該執行緒開始後對該變數的讀可見
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
- 執行緒對 volatile 變數的寫,對接下來其它執行緒對該變數的讀可見
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
- 執行緒解鎖 m 之前對變數的寫,對於接下來對 m 加鎖的其它執行緒對該變數的讀可見
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
- 執行緒結束前對變數的寫,對其它執行緒得知它結束後的讀可見(比如其它執行緒調用 t1.isAlive() 或t1.join()等待它結束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
- 執行緒 t1 打斷 t2(interrupt)前對變數的寫,對於其他執行緒得知 t2 被打斷後對變數的讀可見
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
- 對變數默認值(0,false,null)的寫,對其它執行緒對該變數的讀可見
- 具有傳遞性,如果 x hb-> y 並且 y hb-> z 那麼有 x hb-> z
樂觀鎖與悲觀鎖
這一小節我們來詳細介紹一下樂觀鎖和悲觀鎖的概念以及原型
樂觀鎖與悲觀鎖簡介
我們首先分別簡單介紹一下樂觀鎖和悲觀鎖:
- 樂觀鎖的思想:最樂觀的估計,不怕別的執行緒來修改共享變數,就算改了也沒關係,我繼續重試即可。
- 悲觀鎖的思想:最悲觀的估計,得防著其它執行緒來修改共享變數,針對共享數據直接上鎖,只有我解鎖後你們才能搶奪
我們在這裡再簡單講一下兩種鎖的日常選用:
- 樂觀鎖用於競爭不激烈且為多核CPU的情況,因為其實樂觀鎖的不斷嘗試需要cpu處理並且也會消耗一定記憶體
- 悲觀鎖用於競爭激烈需要搶奪資源的情況下,我們直接停止其他操作可以減少其他不必要的內耗
樂觀鎖實現
樂觀鎖的實現是採用CAS:
- CAS 即 Compare and Swap ,它體現的一種樂觀鎖的思想
我們通過一個簡單示例展示:
// 需要不斷嘗試
while(true) {
int 舊值 = 共享變數 ; // 比如拿到了當前值 0
int 結果 = 舊值 + 1; // 在舊值 0 的基礎上增加 1 ,正確結果是 1
/*
這時候如果別的執行緒把共享變數改成了 5,本執行緒的正確結果 1 就作廢了,這時候
compareAndSwap 返回 false,重新嘗試,直到:
compareAndSwap 返回 true,表示我本執行緒做修改的同時,別的執行緒沒有干擾
*/
if( compareAndSwap ( 舊值, 結果 )) {
// 成功,退出循環
}
}
悲觀鎖實現
樂觀鎖的實現是採用synchronized:
- synchronized體現的是一種悲觀鎖的思想
我們通過一個簡單示例展示:
package cn.itcast.jvm.t4.avo;
// 我們進行操作時,直接上鎖,不允許其他進程涉及!
public class Demo4_1 {
// 這裡的i應該被多執行緒共用,設為靜態變數
static int i = 0;
// 這裡是Obj對象,我們設置它為鎖,注意兩個執行緒中的synchronized所對應的鎖應該是同一個對象(鎖)
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 採用synchronized設置鎖實現原子性,這樣i++操作就會完整進行
Thread t1 = new Thread(() -> {
synchronized (obj) {
for (int j = 0; j < 50000; j++) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
// 採用synchronized設置鎖實現原子性,這樣i--操作就會完整進行
synchronized (obj) {
for (int j = 0; j < 50000; j++) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
原子操作類
其實JUC中為我們提供了原子操作類:
- 可以提供執行緒安全的操作,例如:AtomicInteger、AtomicBoolean等,它們底層就是採用 CAS 技術 + volatile 來實現的。
我們採用改寫之前的一個例子來進行展示:
package cn.itcast.jvm.t4.avo;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo4_4 {
// 創建原子整數對象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); // 獲取並且自增 i++
// i.incrementAndGet(); 自增並且獲取 ++i
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndDecrement(); // 獲取並且自減 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
synchronized 優化
這一小節我們來詳細介紹一下synchronized的優化部分
Mark Word
我們首先來介紹一個概念:
- Java HotSpot 虛擬機中,每個對象都有對象頭(包括 class 指針和 Mark Word)。
那麼我們主要需要這個Mark Word來存儲資訊:
- Mark Word 平時存儲這個對象的哈希碼,分代年齡
- 當加鎖時這些資訊就根據情況被替換為 標記位,執行緒鎖記錄指針,重量級鎖指針,執行緒ID 等內容
輕量級鎖
首先我們先來介紹一下輕量級鎖:
- 如果一個對象雖然有多執行緒訪問,但多執行緒訪問的時間是錯開的(也就是沒有競爭),那麼可以使用輕量級鎖來優化。
我們通過一個簡單案例展示:
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步塊 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步塊 B
}
}
我們會發現即使上述為兩個鎖,但是同時都屬於當前主執行緒下,並且是按順序執行,這是就採用了輕量級鎖
我們通過一個表格寫出其具體流程:
執行緒 1 | 對象 Mark Word | 執行緒 2 |
---|---|---|
訪問同步塊 A,把 Mark 複製到 執行緒 1 的鎖記錄 | 01(無鎖) | – |
CAS 修改 Mark 為執行緒 1 鎖記錄 地址 | 01(無鎖) | – |
成功(加鎖) | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
執行同步塊 A | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
訪問同步塊 B,把 Mark 複製到 執行緒 1 的鎖記錄 | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
CAS 修改 Mark 為執行緒 1 鎖記錄 地址 | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
失敗(發現是自己的鎖) | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
鎖重入 | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
執行同步塊 B | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
同步塊 B 執行完畢 | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
同步塊 A 執行完畢 | 00(輕量鎖)執行緒 1 鎖記錄地址 | – |
成功(解鎖) | 01(無鎖) | – |
– | 01(無鎖) | 訪問同步塊 A,把 Mark 複製到 執行緒 2 的鎖記錄 |
– | 01(無鎖) | CAS 修改 Mark 為執行緒 2 鎖記錄 地址 |
– | 00(輕量鎖)執行緒 2 鎖記錄地址 | 成功(加鎖) |
– | … | … |
鎖的膨脹
我們同樣先來介紹鎖膨脹的概念:
- 如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功
- 這時一種情況就是有其它執行緒為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。
我們直接給出一個表格寫出其具體流程:
程 1 | 對象 Mark | 執行緒 2 |
---|---|---|
訪問同步塊,把 Mark 複製到執行緒 1 的鎖記錄 | 01(無鎖) | – |
CAS 修改 Mark 為執行緒 1 鎖記錄地 址 | 01(無鎖) | – |
成功(加鎖) | 00(輕量鎖)執行緒 1 鎖 記錄地址 | – |
執行同步塊 | 00(輕量鎖)執行緒 1 鎖 記錄地址 | – |
執行同步塊 | 00(輕量鎖)執行緒 1 鎖 記錄地址 | 訪問同步塊,把 Mark 複製 到執行緒 2 |
執行同步塊 | 00(輕量鎖)執行緒 1 鎖 記錄地址 | CAS 修改 Mark 為執行緒 2 鎖 記錄地址 |
執行同步塊 | 00(輕量鎖)執行緒 1 鎖 記錄地址 | 失敗(發現別人已經佔了 鎖) |
執行同步塊 | 00(輕量鎖)執行緒 1 鎖 記錄地址 | CAS 修改 Mark 為重量鎖 |
執行同步塊 | 10(重量鎖)重量鎖指 針 | 阻塞中 |
執行完畢 | 10(重量鎖)重量鎖指 針 | 阻塞中 |
失敗(解鎖) | 10(重量鎖)重量鎖指 針 | 阻塞中 |
釋放重量鎖,喚起阻塞執行緒競爭 | 01(無鎖) | 阻塞中 |
– | 10(重量鎖) | 競爭重量鎖 |
– | 10(重量鎖) | 成功(加鎖) |
– | … | … |
重量級鎖
我們這裡也來簡單介紹一下重量級鎖的優化方法:
- 重量級鎖競爭的時候,還可以使用自旋來進行優化
- 如果當前執行緒自旋成功(即這時候持鎖執行緒已經退出了同步塊,釋放了鎖),這時當前執行緒就可以避免阻塞
我們對自旋進行簡單補充:
- 在 Java 6 之後自旋鎖是自適應的
- 比如對象剛剛的一次自旋操作成功過,那麼認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋
- 自旋會佔用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
- Java 7 之後不能控制是否開啟自旋功能
首先我們給出自旋成功的流程展示:
執行緒 1 (cpu 1 上) | 對象 Mark | 執行緒 2 (cpu 2 上) |
---|---|---|
– | 10(重量鎖) | – |
訪問同步塊,獲取 monitor | 10(重量鎖)重量鎖指針 | – |
成功(加鎖) | 10(重量鎖)重量鎖指針 | – |
執行同步塊 | 10(重量鎖)重量鎖指針 | – |
執行同步塊 | 10(重量鎖)重量鎖指針 | 訪問同步塊,獲取 monitor |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行完畢 | 10(重量鎖)重量鎖指針 | 自旋重試 |
成功(解鎖) | 01(無鎖) | 自旋重試 |
– | 10(重量鎖)重量鎖指針 | 成功(加鎖) |
– | 10(重量鎖)重量鎖指針 | 執行同步塊 |
– | … | … |
然後我們給出自旋失敗的流程展示:
執行緒 1(cpu 1 上) | 對象 Mark | 執行緒 2(cpu 2 上) |
---|---|---|
– | 10(重量鎖) | – |
訪問同步塊,獲取 monitor | 10(重量鎖)重量鎖指針 | – |
成功(加鎖) | 10(重量鎖)重量鎖指針 | – |
執行同步塊 | 10(重量鎖)重量鎖指針 | – |
執行同步塊 | 10(重量鎖)重量鎖指針 | 訪問同步塊,獲取 monitor |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行同步塊 | 10(重量鎖)重量鎖指針 | 自旋重試 |
執行同步塊 | 10(重量鎖)重量鎖指針 | 阻塞 |
– | … | … |
偏向鎖
我們首先來介紹一下偏向鎖:
- 輕量級鎖在沒有競爭時(就自己這個執行緒),每次重入仍然需要執行 CAS 操作,Java 6 中引入了偏向鎖來做進一步優化
- 只有第一次使用 CAS 將執行緒 ID 設置到對象的 Mark Word 頭,之後發現這個執行緒 ID是自己的就表示沒有競爭,不用重新 CAS.
我們給出偏向鎖的一些補充資訊:
- 撤銷偏向需要將持鎖執行緒升級為輕量級鎖,這個過程中所有執行緒需要暫停(STW)
- 訪問對象的 hashCode 也會撤銷偏向鎖
- 如果對象雖然被多個執行緒訪問,但沒有競爭,這時偏向了執行緒 T1 的對象仍有機會重新偏向 T2,重偏向會重置對象的 Thread ID
- 撤銷偏向和重偏向都是批量進行的,以類為單位
- 如果撤銷偏向到達某個閾值,整個類的所有對象都會變為不可偏向的
- 可以主動使用 -XX:-UseBiasedLocking 禁用偏向鎖
我們採用輕量級鎖的程式碼但是加入了偏向鎖之後的流程:
執行緒 1 | 對象 Mark |
---|---|
訪問同步塊 A,檢查 Mark 中是否有執行緒 ID | 101(無鎖可偏向) |
嘗試加偏向鎖 | 101(無鎖可偏向)對象 hashCode |
成功 | 101(無鎖可偏向)執行緒ID |
執行同步塊 A | 101(無鎖可偏向)執行緒ID |
訪問同步塊 B,檢查 Mark 中是否有執行緒 ID | 101(無鎖可偏向)執行緒ID |
是自己的執行緒 ID,鎖是自己的,無需做更多操作 | 101(無鎖可偏向)執行緒ID |
執行同步塊 B | 101(無鎖可偏向)執行緒ID |
執行完畢 | 101(無鎖可偏向)對象 hashCode |
其它優化
我們下面來簡單介紹一下其他的幾種優化:
- 減少上鎖時間
/*
上鎖期間的程式碼是影響上鎖時間的最大因素
我們應該確保同步程式碼塊中盡量短
*/
- 減少鎖的粒度
/*
將一個鎖拆分為多個鎖提高並發度
例如:LinkedBlockingQueue 入隊和出隊使用不同的鎖,相對於LinkedBlockingArray只有一個鎖效率要高
*/
- 鎖粗化
/*
多次循環進入同步塊不如同步塊內多次循環
另外 JVM 可能會做如下優化,把多次 append 的加鎖操作粗化為一次(因為都是對同一個對象加鎖,沒必要重入多次)
例如:new StringBuffer().append("a").append("b").append("c");
*/
- 鎖消除
/*
JVM 會進行程式碼的逃逸分析,例如某個加鎖對象是方法內局部變數,不會被其它執行緒所訪問到,這時候就會被即時編譯器忽略掉所有同步操作。
*/
- 讀寫分離
/*
CopyOnWriteArrayList
ConyOnWriteSet
*/
結束語
到這裡我們JVM的記憶體模型篇就結束了,希望能為你帶來幫助~
附錄
該文章屬於學習內容,具體參考B站黑馬程式設計師滿老師的JVM完整教程
這裡附上影片鏈接:01-JMM-概述_嗶哩嗶哩_bilibili