注:本文參考學習周志明老師的《深入理解Java虛擬機(第3版)》
第10章 前端編譯與優化
10.1 概述
- 前端編譯器就是把* .java檔案轉變成*.class檔案程序,也可能是JIT運行期把位元組碼轉變為本地機器碼的程序,
- 前端編譯器:JDK的javac、Eclipse JDT的增量式編譯器
- 即時編譯器:HotSpot的C1和C2編譯器、Graal編譯器,
- 提前編譯器:JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET
10.2 Javac編譯器
10.2.1 Javac的原始碼與除錯
-
Javac后期被挪到了jdk.compiler模塊
-
javac編譯程序分為一個準備程序和3個處理程序
- 準備程序:初始化插入式注解處理器
- 決議與填充符號
- 詞法、語法分析
- 填充符號表
- 插入式注解處理器處理程序,
- 分析與位元組碼生成程序
- 標注檢測
- 資料流及控制流分析,對程式對動態運行程序分析
- 解語法糖,簡化代碼撰寫的語法糖還原原來的形式
- 位元組碼生成,把前面步驟的資訊轉換成位元組碼,
-
javac編譯的程序,

javac的編譯入口
- JavaCompiler類完成編譯,上面的動作集中在compile()和compile2()

10.2.2 決議與填充符號表
- parseFiles()就是語法分析和詞法分析,
1.詞法、語法分析
- 詞法分析是把源代碼的字符流轉變為標記集合的程序,單個字符是最小的程式撰寫單位,標記才是編譯的最小的元素,關鍵詞,字面量,運算子都是標記,通過Scanner類實作,int a=1,分為int、a和1.
- 語法分析根據標記序列抽象為語法樹,
2.填充符號表
- enterTrees(),符號表由一組符號地址和符號資訊構成的資料結構,
- 符號表登記的資訊編譯不同階段需要用到,通過Enter類實作,
10.2.3 注解處理器
- 插入式注解處理器可以看做是編譯器的插件,插件作業允許讀取,修改,添加抽象語法樹的元素,
- 有了編譯器注解處理標準API,程式員才能干涉編譯器的行為,比如Lombok,
10.2.4 語意分析與位元組碼生成
- 經過語法分析,編譯器獲得了程式代碼的抽象語法樹的表示,抽象語法樹可以表示結構正確的源程式,但是無法表示源程式的語意是符合邏輯的,
- 語意分析根據結構背景關系檢查語意,
- 編碼的紅線標注錯誤就是語意分析的結果,
1.標注檢查
- 語意分析分為標注檢查和資料及控制流分析
- 標注檢查需要檢查變數使用前是否被宣告,變數、賦值之間的資料型別是否能夠匹配,
- 還有常量折疊優化,比如a=1+2,優化之后就直接是3了,
2.資料及控制流分析
- 資料流分析和控制流分析是對程式的背景關系邏輯進一步驗證,
- 檢查區域變數使用前是否賦值,方法每條路徑是否有回傳值,是否所有受查例外都被正常處理,
- 可以看做和類的加載時的資料與控制流的分析目的是一樣的,但是校驗范圍不同,
3.解語法糖
- 語法糖可以減少代碼量,增加程式的可讀性,減少程式出錯的機會,
- 常見的語法糖
- 泛型,
- 變長引數
- 自動裝箱拆箱,
- Java虛擬機并不是直接支持這些語法的,他們會在編譯的階段還原回原始的基礎語法結構,這個就是解語法糖,
4.位元組碼生成
- Javac的最后一個階段,包括生成位元組碼,
10.3 Java語法糖的味道
10.3.1 泛型
- 泛型的本質是引數化型別和引數化多型的應用,
1.Java與C#的泛型
- Java選擇泛型的實作是型別擦除式泛型,C#實作的是具體化式泛型,
- C#的List< string >和List< int >有自己獨立的虛方法表,和型別資料,
- Java語言的泛型,只在原始碼存在,編譯后的位元組碼檔案中,泛型都被替換了,
3.型別擦除
- 型別擦除需要裸型別,它是所有的泛型化實體的共同父型別,
- 實際上就是從Hashmap< String>到HashMap,也就是插入元素的時候,插入的時候從Object轉換為String,
- 泛型遇到多載的時候,就會發現是不行的,原因就是由于型別擦除導致引數看上去不同,當編譯之后其實就是一樣的了,
10.3.2 自動裝箱、拆箱與遍歷回圈
- 這是編譯之前的,
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
- 編譯之后的裝箱補充,
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
第11章 后端編譯與優化
11.1 概述
- 編譯器無論在何時、在何種狀態把Class檔案轉換與本地基礎設施相關的二進制機器碼,這個就可以視為編譯程序的后端,
- 位元組碼只是程式語言的中間表達程序,
11.2 即時編譯器
- JIT是為了解決熱點代碼等的優化,把代碼編譯為本地機器碼,而不是以前那樣看一句解釋一句,
- 問題
- 為何HotSpot虛擬機要使用解釋器與即時編譯器并存的架構?
- 為何HotSpot虛擬機要實作兩個(或三個)不同的即時編譯器?
- 程式何時使用解釋器執行?何時使用編譯器執行?
- 哪些程式代碼會被編譯為本地代碼?如何編譯本地代碼?
- 如何從外部觀察到即時編譯器的編譯程序和編譯結果?
11.2.1 解釋器與編譯器
解釋器與編譯器各自的優勢
- 程式需要迅速啟動和執行的時候,解釋器首先發揮作用,省去編譯的時間,立刻運行,
- 但是程式運行之后,編譯器的作用是把代碼編譯為本地代碼,減少解釋器的中間損耗,

即時編譯器的型別
- HotSpot通常是內置了兩個到三個即時編譯器,分別稱為客戶端編譯器和服務器端編譯器,
- 也就是C1和C2編譯器,
- 通常是解釋器與即時編譯器混合作業,如果不想那么可以通過
- -Xint強制虛擬機只能夠運行解釋模式
- -Xcomp可以強制虛擬機運行編譯模式,
即時編譯器的分層
- 由于JIT編譯需要時間,而且需要解釋器收集監控資訊,毫無疑問是非常損耗性能的
- 所以需要分層編譯功能,
- 第0層,純解釋,解釋器不開啟性能監控
- 第1層,客戶端把位元組碼編譯為本地代碼運行,不開啟性能監控
- 第2層:客戶端編譯器+僅開啟方法和回邊次數統計有限的性能監控功能,
- 第3層:客戶端編譯器+開啟全部的性能監控功能,還會收集分支跳轉、虛方法呼叫等,
- 第4層:使用服務器端,把位元組碼編譯為本地代碼,它能夠開啟更多優化,但是可能會激進優化,
- 使用客戶端編譯器可以獲取更高的編譯速度,但是服務器端的編譯器能夠有更好的編譯質量,
- 通常需要-XX:TieredCompilation來開啟分層編譯,

11.2.2 編譯物件與觸發條件
- 熱點代碼
- 多次呼叫的方法
- 被多次執行的回圈體,
堆疊上替換的定義,
- 對于兩種情況,編譯目標是整個方法體而不是回圈體,
- 第一種情況編譯整個方法
- 后一種也是編譯整個方法,只是執行入口(從第幾條位元組碼開始執行)不同,編譯的時候傳入執行入口位元組碼序號(Byte Code Index,BCI)這種編譯在方法執行程序中,可以被稱為堆疊上替換,方法的堆疊幀還在堆疊上,但是方法被替換了,意思就是方法的執行入口已經是開始執行編譯器編譯好的代碼,而不是解釋執行了,方法在堆疊,但是方法被編譯器替換,所以是堆疊上替換,
- 那么怎么才算是被執行多次?是不是要即時編譯,這個行為就是熱點探測,
- 采樣法,周期檢測執行緒的呼叫堆疊頂,缺點是很難知道方法的熱度
- 計數器,統計執行方法的執行次數,維護很難,不能直接獲取方法的呼叫關系,
- 虛擬機使用的是基于計數器的熱點探測,所以Hotspot配備了方法呼叫計數器和回邊計數器,如果計數器溢位就會觸發即時編譯,
熱點探測的計數器
- 方法呼叫計數器,統計方法執行次數,閾值可以通過-XX:CompileThreshold來調整,并且呼叫方法的時候首先看看方法是不是被即時編譯過,如果是,優先使用本地代碼,如果超過閾值也會發送編譯請求,執行引擎默認不會等待,而是繼續解釋執行,知道方法的入口地址被系統修改,為新值,
- 方法呼叫計數器只是計算一段時間的,如果一段時間沒有達到閾值就會減少一半,這個就是半衰周期,在垃圾回收的同時一起執行,可以使用虛擬機引數-XX:UseCounterDecay關閉熱度衰減,
- 還可以使用-XX:CounterHalfLifeTime引數設定為半衰的周期時間,

回邊計數器
- 統計方法體回圈體的執行次數,位元組碼遇到控制流向后跳轉的指令就是回邊,為了觸發堆疊上的替換編譯,
- 閾值設定-XX:CompileThreshold的引數-XX:BackEdgeThreshold,
- 但是用戶需要設定另一個引數-XX:OnStackReplacePercentage調整回邊計數器的閾值,
- 解釋器遇到一潭訓邊指令的時候,檢查是不是有編譯好的版本,如果有那么就優先使用,否則就是回邊計數器+1,超過閾值的時候就發送堆疊上替換的請求,并且調整回邊計數器的閾值,
- 回邊計數器沒有衰減的程序,而且是絕對次數,

11.2.3 編譯程序
- 虛擬機在沒有完成編譯的時候都是解釋執行,編譯動作在后臺完成,
- 也可以通過-XX:BackgroundCompilation禁止后臺編譯,那么虛擬機繼續執行就必須阻塞等待編譯,
那在后臺執行編譯的程序中,編譯器具體會做什么事情呢?
- 客戶端關注的是區域優化,放棄耗時比較長的全域優化,而且他是三階段編譯器
- 第一個階段,獨立前端把位元組碼構成一種高級中間代碼表示(HIR),HIR使用靜態分配代表代碼值,
- 第二個階段:后端從HIR中產生低級的中間代碼表示(LIR),在這之前會優化HIR,空值消除等,范圍檢查,
- 最后的階段就是后端的線性掃描,在LIR分配暫存器,并在LIR做窺孔優化,產生機器碼,

- 服務器端的編譯采用的暫存器是全域圖著色分配器,能夠充分利用處理器的架構,它的編譯很慢,
11.2.4 實戰:查看及分析即時編譯結果
- 下面是測驗代碼,需要加上-XX:+PrintCompilation,如果有%說明進行了堆疊上分配,
- 這里的代碼邏輯實際上都行內到了main方法里面,
public class MyTest {
public static final int NUM = 15000;
public static int doubleValue(int i) {
// 這個慷訓圈用于后面演示JIT代碼優化程序
for(int j=0; j<100000; j++);
return i * 2;
}
public static long calcSum() {
long sum = 0;
for (int i = 1; i <= 100; i++) {
sum += doubleValue(i);
}
return sum;
}
public static void main(String[] args) {
for (int i = 0; i < NUM; i++) {
calcSum();
}
}
}
11.3 提前編譯器
11.3.1 提前編譯的優劣得失
- 提前編譯的兩個分支
- 程式運行之前把代碼編譯成機器碼的靜態翻譯作業
- 另一條分支是把原本即時編譯器在運行要做的編譯作業提前做好并且保存下來,下次用的時候加載進來使用,
- 第一條是傳統的提前編譯應用形式,JIT始侄訓是會占用運算資源和運算的時間,
- 第二條路徑,本質就是給即時編譯器做一個快取加速,這種是動態提前編譯,即時編譯快取,
即時編譯器的優勢
- 性能分析制導優化,通過收集性能資料來優化,能夠更好地分配資源,
- 激進預測性優化
- 鏈接時優化,
11.4 編譯器優化技術
11.4.1 優化技術概覽
- 下面的代碼看上去已經很簡潔了,但是仍有優化的空間,
- 第一個是方法內斂,去除方法呼叫創建堆疊幀的成本
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
// ...do stuff...
z = b.get();
sum = y + z;
}
- 這個就是優化之后的代碼,
public void foo() {
y = b.value;
// ...do stuff...
z = b.value;
sum = y + z;
}
- 第二步就是訪問冗余消除,
- 這樣的好處就是不需要訪問物件b的區域變數,
public void foo() {
y = b.value;
// ...do stuff...
z = y;
sum = y + z;
}
- 第三步復寫傳播,很明顯就是z和y相同,那么用y代替z,
public void foo() {
y = b.value;
// ...do stuff...
y = y;
sum = y + y;
}
- 第四次優化是無用代碼消除,這里的y=y沒有任何意義,
public void foo() {
y = b.value;
// ...do stuff...
sum = y + y;
}
11.4.2 方法行內
- 方法行內可以減少方法呼叫帶來的成本,為其它優化手段建立良好的基礎,如果不做內斂可能就無法進行進一步的優化,可以參考上面的例子,
- 無法內斂的原因
- invokespecial呼叫私有方法、實力構造器、父類方法和invokestatic呼叫的靜態方法才會在編譯期決議,
- 其它必須是虛方法,虛方法很難決定哪個是正確的方法版本,
- 為了解決虛方法問題,Java虛擬機引入了型別繼承關系分析技術,
- 用于確定類的某個介面有多于一種的實作,
- 某個類是不是存在子類覆寫父類的某個虛方法,
- 如果不是虛方法直接行內
- 如果是通過CHA也就是型別繼承關系分析技術來查詢,
- 如果發現只有一種狀態的實作,那么這種就是守護行內
- 否則就是激進的預測性優化,需要逃生門,也就是預測失敗的退路,
- 或者另外一種方式就是行內快取,減少方法呼叫的開銷,
- 作業原理是第一呼叫方法會記錄方法接受者的版本,然后每次呼叫都比較版本,如果版本一樣那么就是單態行內快取,
- 如果發現版本不同,就會退化為多型行內快取,還是要通過查找虛方法表來進行分派,
11.4.3 逃逸分析
- 基本原理是分析物件的動態作用域,當一個物件在方法定義好之后,被外部方法呼叫,比如作為引數傳遞到別的方法,這種就是方法逃逸,還有可能被外部執行緒訪問,這種是執行緒逃逸,
- 根據逃逸程度的不同優化介紹
- 堆疊上分配:如果確定物件不會逃逸出執行緒之外,那讓物件在堆疊上分配也會是不錯的選擇,因為物件占用的記憶體,隨著堆疊幀出堆疊銷毀,由于物件在堆空間的回收消耗大量的資源,所以才會提出堆疊上分配,堆疊上分配支持方法逃逸,但是不支持執行緒逃逸,只要不逃出執行緒之外,那么就可以使用堆疊上分配,
- 標量替換:資料不能再拆分,比如基礎型別int和long這些就是標量,如果可以拆解那么就是聚合量,物件就是聚合量,根據程式情況,把物件的成員變數恢復原始型別訪問,這個就是標量替換,如果物件不被方法外部訪問,而且物件可以拆散,那么程式執行的時候,不去創建物件,而是創建方法使用的成員變數代替,這樣能夠為后續優化提供條件,它是不允許物件逃逸到方法之外的,
- 同步消除:變數如果不會逃逸出執行緒,那么這個變數的讀寫肯定不會有競爭,所以這個變數的同步措施可以消除,
逃逸分析作業程序
// 完全未優化的代碼
public int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}
- 第一步P的建構式和getX()行內優化,
// 步驟1:建構式行內后的樣子
public int test(int x) {
int xx = x + 2;
Point p = point_memory_alloc(); // 在堆中分配P物件的示意方法
p.x = xx; // Point建構式被行內后的樣子
p.y = 42
return p.x; // Point::getX()被行內后的樣子
}
- 第二步經過逃逸分析,發現test()范圍物件實體p不會逃逸,那么進行一個標量替換,
- 實際上就把p.x和p.y變成普通的基礎變數,
// 步驟2:標量替換后的樣子
public int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42
return px;
}
- 第三步根據資料流分析,py值對方法不造成影響,所以無效代碼消除優化,
// 步驟3:做無效代碼消除后的樣子
public int test(int x) {
return x + 2;
}
開啟逃逸分析
- -XX:DoEscapeAnalysis開啟逃逸分析,
- -XX:+EliminateAllocations:開啟標量替換
- -XX:+EliminateLocks:同步消除
11.4.4 公共子運算式消除
- 運算式E被計算過,而且E的所有變數值沒有改變,那么E的出現就是公共子運算式,沒有必要重新計算,
int d = (c * b) * 12 + a + (a + b * c);
iload_2 // b
imul // 計算b*c
bipush 12 // 推入12
imul // 計算(c * b) * 12
iload_1 // a
iadd // 計算(c * b) * 12 + a
iload_1 // a
iload_2 // b
iload_3 // c
imul // 計算b * c
iadd // 計算a + b * c
iadd // 計算(c * b) * 12 + a + a + b * c
istore 4
- 上面沒有做任何優化的代碼,
- 如果加入了即時編譯器,檢測到b*c和c *b是一個運算式那么就會優化為int d = E * 12 + a + (a + E);
- 還能夠代數化簡int d = E * 13 + a + a;能夠節省很多的計算時間,減少了計算的指令,
11.4.5 陣列邊界檢查消除
- 對于每次陣列元素訪問都需要一次越界判斷是非常消耗性能的,
- 但是陣列檢測越界是不是每次都要檢測?
- 可以通過對資料流的分析回圈訪問,如果在合理范圍那么就不會進行越界檢測,
- 空指標檢測和除數是0的檢測采用了隱式的例外處理,
if (foo != null) {
return foo.value;
}else{
throw new NullPointException();
}
- 經過處理之后
try {
return foo.value;
} catch (segment_fault) {
uncommon_trap();
}
- 虛擬機會注冊一個Segment Fault信號例外處理器,當foo是空的時候才會轉到例外處理器中恢復中斷并且拋出例外,
第12章 Java記憶體模型與執行緒
12.2 硬體的效率與一致性
- 高速快取解決了處理器與記憶體之間的矛盾,但是帶來了新的問題:快取一致性,

12.3 Java記憶體模型
- Java記憶體模型的作用是屏蔽各種硬體和作業系統的記憶體訪問差異,并且實作Java程式的各個平臺能夠達到一致的記憶體訪問效果,
12.3.1 主記憶體與作業記憶體
- 作業記憶體是執行緒獨有的,
- 主記憶體是執行緒共享的,
12.3.2 記憶體間互動操作
- lock(鎖定):作用于主記憶體的變數,它把一個變數標識為一條執行緒獨占的狀態,
- unlock(解鎖):作用于主記憶體的變數,它把一個處于鎖定狀態的變數釋放出來,釋放后的變數 才可以被其他執行緒鎖定,
- read(讀取):作用于主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的作業記憶體中,以 便隨后的load動作使用,
- read(讀取):作用于主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的作業記憶體中,以 便隨后的load動作使用,
- use(使用):作用于作業記憶體的變數,它把作業記憶體中一個變數的值傳遞給執行引擎,每當虛 擬機遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作,
- assign(賦值):作用于作業記憶體的變數,它把一個從執行引擎接收的值賦給作業記憶體的變數, 每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作,
- store(存盤):作用于作業記憶體的變數,它把作業記憶體中一個變數的值傳送到主記憶體中,以便隨 后的write操作使用,
- write(寫入):作用于主記憶體的變數,它把store操作從作業記憶體中得到的變數的值放入主記憶體的 變數中,
- 如果要把主記憶體拷貝到作業記憶體就要按照一定的順序,read和load,store之后才能write,
- Java記憶體模型的八個規則
- 不允許read、load和store、write調換位置,
- 不允許執行緒丟棄最近的assign行為,
- 不允許執行緒沒有發生assign的時候把作業記憶體同步到主記憶體,
- 新變數只能在主記憶體誕生,
- 一個變數同一時刻只能被一個執行緒lock,
- 變數必須lock之后才能unlock
- 對變數unlock必須要同步到主記憶體,
12.3.3 對于volatile型變數的特殊規則
- 兩項特性
- 可見性
- 有序性,禁止指令重排序,
- volatile修飾的變數的賦值多了一個lock addl$0x0,(%esp)操作,相當于就是一個記憶體屏障,重排序的時候不能夠把
- lock的作用是把快取寫入到記憶體,寫入的同時也會把別的內核快取無效化,也就是Java記憶體模型的store和write操作,并且這樣的空操作可以讓volatile變數的修改對其他的處理器可見,
- 為什么可以禁止指令重排序?lock指令能夠保證之前的操作一定是完成的,指令重排序是無法越過這條指令的,
- volatile讀操作和普通的變數讀差不多,但是寫操作會慢一些,因為需要插入很多的記憶體屏障保證處理器不亂序執行,但是volatile總開銷還是要少一些,
案例,
- T是一個執行緒,V和W分別是volatile變數,進行read、load、use、assign、store、write操作需要滿足的規則,
- T對V變數前一個操作是load的時候,執行緒T才能夠對變數V進行use動作,并且只有當執行緒T對變數V執行的后一個動作是use的時候,執行緒T才能對V進行load操作,use可以說一定要load和read是一起出現的,
- 也就是上面的規則必須要讓使用V之前,必須從主記憶體重繪最新的值,保證對其他執行緒可見,
- 只有執行緒T對變數V執行的前一個操作是assign的時候,執行緒T才能夠對變數V執行store操作,反之一樣,也就是assign是執行緒T對V的store、write動作相關聯的,必須連續三個出現,
- 上面這條規則保證了每次修改V之后必須同步會主記憶體,保證其他執行緒對變數V的修改,
- 假定動作A是執行緒T對變數T執行的assign或者是use動作,動作F是A相關聯的load或者是store動作,假定動作P是動作F相對應的V的read和write操作,動作B是執行緒T對變數W的執行的use或者是assing操作,假定動作G是和動作B相關聯的load或store動作,假定動作Q是動作G的相應的變數W的read或者是write操作,如果A先于B,那么P一定是先于Q的,
- 這條規則要求volatile變數不會被指令重排序,保證代碼的執行順序與程式順序相同,
- T對V變數前一個操作是load的時候,執行緒T才能夠對變數V進行use動作,并且只有當執行緒T對變數V執行的后一個動作是use的時候,執行緒T才能對V進行load操作,use可以說一定要load和read是一起出現的,
12.3.4 針對long和double型變數的特殊規則
- Java記憶體模型要求lock、unlock、read、load、assign、use、store、write有原子性,
- 但是對于64位資料型別來說,允許沒有被volatile修飾的變數可以劃分兩次32位操作執行,也就是允許虛擬機可以自行選擇是否保證load、store、read、write的原子性,也就是long和double的非原子性的協定,
12.3.5 原子性、可見性與有序性
- 原子性
- Java記憶體模型直接保證的原子性是read、load、assign、use、store、write,也就是基本資料型別的訪問和讀寫都是具備原子性的,
- 更大范圍的原子性可以是lock或者unlock完成,
- 可見性(Visibility)
- 可見性就是執行緒修改共享變數之后,其它執行緒可以立即得知這個修改,
- Java記憶體模型通過修改后把新的值同步回主記憶體,在變數讀取之前必須從主記憶體讀取,
- 同步塊的可見性對變數執行unlock之前,必須把變數同步到主記憶體,(store、write)這條規則獲得的,
- final關鍵詞修飾的欄位的可見性是構造器初始化完成,構造器沒有把this的參考傳遞出去,避免只看到一半的初始化,
- 有序性(Ordering)
- Java記憶體模型的天然有序性,執行緒內的操作有序,但是從另一個執行緒看另一個執行緒操作是無序的,
- 前半句是執行緒內表現類似串行,
- 后半句是指令重拍序與作業記憶體與主記憶體延遲的現象,
12.3.6 先行發生原則
- 這個原則是判斷資料是否存在競爭,執行緒是否安全的手段,
- 先行發生指的是Java記憶體模型定義的兩項操作之間的偏序關系,操作A比操作B先行發生,在操作B之前操作A產生的影響可以被操作B觀察到,
- Java記憶體模型的天然先行關系,
- 程式次序規則:就是按照執行緒內的控制流順序執行,
- 管程鎖定規則:同一個鎖unlock在lock之前,
- volatile變數規則:volatile的寫操作先行于后面變數的讀操作
- 執行緒啟動規則:Thread的start方法先行于執行緒內的所有動作,
- 執行緒終止規則:執行緒的所有操作先行于執行緒的終止檢測,
- 執行緒中斷規則:對執行緒的interrupt方法呼叫先行于被中斷的執行緒的代碼檢測中斷事件的發生,
- 物件終結規則:一個物件初始化完成先行于它的finalize方法,
- 傳遞性:操作A先行于操作B,操作B先行于操作C,可以得出A也先行于C,
12.4 Java與執行緒
12.4.1 執行緒的實作
- Java語言的執行緒都是統一使用Thread,已經呼叫過start的Thread就代表一個執行緒,所有方法都是native,
- native方法意味著方法無法使用平臺無關的手段實作
- 實作執行緒的方式
- 內核執行緒
- 用戶級執行緒
- 用戶執行緒+輕量級行程,
- 內核執行緒實作
- 內核執行緒的實作方式就是1:1,內核執行緒是作業系統直接支持的執行緒,內核來切換執行緒,把執行緒的任務映射到處理器上,
- 程式一般不使用內核執行緒,而是使用內核執行緒的一種高級介面輕量級行程(LWP),每個輕量級行程都由一個內核執行緒支持,
- 由于內核執行緒的支持,輕量級行程在系統呼叫被阻塞的時候不會影響行程的繼續作業,
- 輕量級行程的問題
- 用戶態和和心態的來回切換,
- 每個輕量級的行程消耗一定的內核資源,

- 用戶執行緒實作
- 用戶執行緒是1:N,一個執行緒不是內核執行緒肯定就是用戶級執行緒,輕量級行程也屬于用戶執行緒,但是輕量級行程建立在內核之上,很多操作都需要系統呼叫,
- 系統內核無法感知用戶執行緒的存在,用戶執行緒的切換,銷毀都在用戶態,執行緒切換可能不需要切換到內核態,操作快速而且低消耗,
- 但是用戶執行緒沒有系統內核的支援,所有的執行緒操作都是用戶程式自己處理,程式的創建,銷毀,切換和調度都是用戶需要考慮的,
- 作業系統只會把處理器資源分配給行程,也就是阻塞如何處理還有多執行緒處理系統如何把執行緒映射到處理器也是一個問題,

- 混合實作
- 內核執行緒與用戶執行緒一起使用,存在用戶執行緒+輕量級執行緒,作業系統支持輕量級的行程作為用戶執行緒和核心執行緒的橋梁,能夠通過內核支持調度和處理器的映射,用戶執行緒的系統呼叫可以通過輕量級行程來完成,

- Java執行緒的實作
- Java虛擬機執行緒模型是基于作業系統原生的執行緒模型1:1實作,
- HotSpot每一個Java執行緒都是直接映射到作業系統的原生執行緒實作,HotSpot不會干擾執行緒的調度,
- Solaris平臺的HotSpot虛擬機支持1:1和N:M的執行緒模型,
12.4.2 Java執行緒調度
- 執行緒調度的方式
- 協同式
- 搶占式,
- 協同式的好處就是執行緒把事情干完之后才切換,切換對執行緒自己是可知的,但是執行緒的執行時間不可控,如果執行緒出問題,不告知系統導致一直不切換,
- 搶占式,執行緒的時間通過系統分配,
12.5 Java與協程
12.5.1 內核執行緒的局限
- Java并發機制與多個分布式服務的快速執行計算出結果出現了矛盾,
- 1:1的內核執行緒模型切換的缺點就是切換與調度的成本太高,系統容納的執行緒數量有限,在現在請求需要執行時間變短,處理請求變多的情況下,用戶執行緒的切換開銷可能接近計算的開銷,
12.5.2 協程的復蘇
- 內核執行緒切換導致的開銷為什么這么大?
- 內核執行緒的調度成本主要來自于用戶態和核心態之間的狀態切換,狀態切換的開銷是回應中斷,保護和恢復執行現場的成本,
- 那么切換成用戶執行緒是不是就解決問題了?
- 答案是不能,但是把保護和恢復現場交給程式員,那么有機會減少這些開銷,
- 最初的用戶執行緒是協同式調度,所以別名是協程,協程可以完整呼叫堆疊保護和恢復作業,
- 協程比較輕量級,實作在用戶態的切換與保護,
- 協程的局限是應用層實作的內容很多,
12.5.3 Java的解決方案
- 有堆疊協程的一種實作是纖程,做執行緒保護,恢復和纖程調度,
第13章 執行緒安全與鎖優化
13.2 執行緒安全
- 多個執行緒同時訪問一個物件,如果不用考慮這些執行緒在運行時環境下的調度和交替進行,也不需要額外的同步,或者是呼叫方進行任何其它的協調操作的時候,呼叫這個物件的行為都能獲取到正確的結果,那么物件就是執行緒安全的,
13.2.1 Java語言中的執行緒安全
- 不可變
- 不可變的物件一定是執行緒安全的,
- 只要一個不可變物件被正確構建,那么外部的可見性永遠不會變化,
- Java語言的基本資料型別只要是final修飾就能夠保證不可變,
- 如果是物件本身,必須保證自身的變數不可變,
- 絕對執行緒安全
- Vector雖然方法都是同步方法,但是不代表兩個方法的分別呼叫是能夠執行緒安全的
- 相對執行緒安全
- 保證單次操作執行緒安全就可以了,
- 執行緒兼容
- 執行緒兼容指可以通過同步手段來保證物件在并發環境可以安全使用,
- 執行緒對立
- 執行緒對立就是無論是否采取了同步手段,都無法在多執行緒里面并發使用代碼,
13.2.2 執行緒安全的實作方法
- 互斥同步
- 同步保證共享資料在同一個時刻只被一個執行緒呼叫,
- 互斥是實作同步的一個手段,臨界區,互斥量,信號量都是互斥的常見實作,
- 互斥是方法,同步是目的,
- Java語言的同步互斥就是synchronized關鍵字實作,在經過編譯之后會在同步塊前后生成monitorenter和monitorexit,兩個位元組碼指令都需要一個reference引數來指明鎖定和解鎖的物件,
- monitorenter首先需要嘗試獲取物件的鎖,如果獲取成功鎖的計數器+1,monitorexit就會-1,計數器只要是0,那么鎖就會被釋放了,
- synchronized是可重入的,
- synchronized在持有鎖的執行緒執行完畢釋放鎖之前,無條件阻塞后面的執行緒,
- 持有鎖是一個重量級的操作,
- 后面通過了Lock介面實作了互斥同步,
- 非阻塞同步
- 互斥同步的問題是執行緒阻塞和喚醒帶來的代價,這種同步也可以說是阻塞同步,互斥同步是一個悲觀的并發策略,無論資料是否共享都會加鎖,
- 后來可以實作樂觀的并發策略,可以先進行操作,如果沒有執行緒爭用共享資料那么操作直接成功,如果失敗就重試,
- 無同步方案
- 同步是保證存在共享資料爭用正確的手段,
- 不需要同步措施
- 可重入代碼,代碼執行可以被中斷,轉去執行別的代碼,控制權回來之后原來的程式不會出錯,
- 執行緒本地存盤,也就是資料無需共享,
13.3 鎖優化
13.3.1 自旋鎖與自適應自旋
- 互斥同步對性能最大的影響是阻塞,掛起,恢復執行緒等,
- 所以后來引入了請求鎖的執行緒可以自旋,執行緒等一等,看看能不能獲取鎖,進入忙等待,
- 它占用處理器時間,所以自旋時間需要限制,
13.3.2 鎖消除
- 同步塊可能會被消除,主要根據逃逸分析,
13.3.3 鎖粗化
- 把同步塊范圍變大,如果鎖加載回圈體里面,而且沒有執行緒競爭就會導致性能的消耗很大,
13.3.4 輕量級鎖
- 沒有多執行緒的競爭下,減少重量級鎖的使用作業系統的互斥量產生的性能消耗,
- 輕量級鎖的作業流程
- 虛擬機在當前執行緒的堆疊幀創建一個鎖記錄空間,存盤物件的Mark Word,并且把物件的Mark Word更新為Lock Record,表示執行緒已經擁有了這個鎖,Mark Word的鎖標志位變成了00,
- 如果更新失敗,先檢查物件的Mark Word是否指向執行緒的堆疊幀,如果不是那么就要膨脹為重量級鎖,鎖標志變為10,Mark Word指向了重量級鎖,
- 解鎖也是CAS,如果發現物件的Mark Word還是指向執行緒的記錄,那么就CAS替換回來,如果失敗說明有別的執行緒在嘗試獲取鎖,釋放鎖的同時喚醒下一個執行緒,

13.3.5 偏向鎖
- 它的目的是無競爭的狀態下的同步原語,提升程式的性能, 偏向鎖在無競爭的情況下把同步都消除了, CAS不需要處理,
- 它會偏向第一個獲取它的執行緒,
- 偏向鎖的作業原理
- CAS操作把獲取鎖的執行緒的ID記錄在物件的Mark Word,CAS成功的話持有偏向鎖的執行緒進入同步塊,虛擬機不需要進行同步操作,
- 一旦有競爭,偏向模式結束,根據物件目前是否處于鎖定狀態來決定是否撤銷偏向,撤銷標志位為未鎖定或者是輕量級鎖定狀態,
- 執行緒ID占用了之前的Mark Word的hashcode,也就是獲取hashcode之后就無法使用偏向鎖了,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/379470.html
標籤:java
