1. 程式人生 > >《深入理解 Java 虛擬機器》筆記整理

《深入理解 Java 虛擬機器》筆記整理

正文

一、Java 記憶體區域與記憶體溢位異常

1、執行時資料區域

  • 程式計數器:當前執行緒所執行的位元組碼的行號指示器。執行緒私有。
  • Java 虛擬機器棧:Java 方法執行的記憶體模型。執行緒私有。
  • 本地方法棧:Native 方法執行的記憶體模型。執行緒私有。
  • Java 堆:存放物件例項。分為新生代(Eden 空間、From Survivor 空間、To Survivor 空間)和老年代。執行緒共享。
  • 方法區:儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。也稱為“永久代”。執行緒共享。
  • 執行時常量池:方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。 執行緒共享。
  • 直接記憶體。

2、物件的建立

類載入檢查 -> 分配記憶體 -> 初始化零值 -> 設定物件頭 -> 執行 init 方法。

  • 類載入檢查:檢查 new 指令的引數能否在常量池中定位到一個類的符號引用,以及這個符號引用代表的類是否已被載入、解析和初始化過。
  • 分配記憶體:把一塊確定大小的記憶體從 Java 堆中劃分出來。
  • 初始化零值:將分配到的記憶體空間初始化為零值(不包括物件頭)。
  • 設定物件頭:虛擬機器需要對物件進行必要的設定,這些資訊存放在物件的物件頭中。
  • 執行 init 方法:把物件按照程式設計師的意願進行初始化。

3、物件的記憶體佈局

  • 物件頭:
    • Mark Word:儲存物件自身的執行時資料。
    • 型別指標:儲存物件的類元資料的指標。
  • 例項資料:物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。
  • 對齊填充:僅僅起著佔位符的作用。

4、物件的訪問定位

  • 控制代碼:引用中儲存的是物件的控制代碼地址。Java 堆中劃分出一塊記憶體作為控制代碼池,控制代碼中包含了物件例項資料、型別資料兩者的具體地址資訊。
  • 直接指標:引用中儲存的直接就是物件的地址。

5、OutOfMemoryError 異常

  • Java 堆溢位
  • 虛擬機器棧和本地方法棧溢位
  • 方法區和執行時常量池溢位
  • 本機直接記憶體溢位

二、垃圾收集器與記憶體分配策略

1、判斷物件是否可用

  • 引用計數演算法:給物件新增一個引用計數器,每當有一個地方引用它時,計數器值加 1;當引用失效時,計數器值減 1;任何時刻計數器為 0 的物件就是不可能再被使用的。
  • 可達性分析演算法:通過一系列被稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到 GC Roots 沒有任何引用鏈相連時,則此物件不可用。

2、四種引用

  • 強引用:類似“Object obj = new Object()”的引用。只要強引用還存在,物件就永遠不會回收。
  • 軟引用:用來描述一些還有用但並非必需的物件。記憶體不足時,物件有可能被回收。
  • 弱引用:用來描述非必需的物件,但強度比軟引用弱。GC時,無論記憶體是否足夠,物件都會被回收。
  • 虛引用:也稱幽靈引用或幻影引用,虛引用不會對物件的生存時間構成影響。虛引用的唯一作用就是能在物件被回收時收到一個系統通知。

3、垃圾收集演算法

  • 標記-清除演算法:分為“標記”和“清除”兩個階段。首先標記出所有需要回收的物件,然後再統一回收所有被標記的物件。會產生大量不連續的記憶體碎片。
  • 複製演算法:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊。當一塊記憶體用完時,就將還存活的物件複製到另一塊,然後再把已使用過的記憶體空間一次清理掉。
  • 標記-整理演算法:首先標記出所有需要回收的物件,然後將所有存活物件向一端移動,最後直接清理掉端邊界以外的記憶體。
  • 分代收集演算法:根據物件存活週期的不同,將 Java 堆劃分為新生代和老年代,然後根據各個年代的特點採用最適當的收集演算法。
    • 新生代:採用複製演算法。
    • 老年代:採用“標記-清除”或“標記-整理”演算法。

4、垃圾收集器

  • Serial 收集器:單執行緒。新生代收集器。
  • ParNew 收集器:Serial 收集器的多執行緒版本。新生代收集器。
  • Parallel Scavenge 收集器:多執行緒。新生代收集器。關注吞吐量。
  • Serial Old 收集器:Serial 收集器的老年代版本。單執行緒。使用“標記-整理”演算法。
  • Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本。多執行緒。使用“標記-整理”演算法。
  • CMS 收集器:併發收集器。使用“標記-清除”演算法。關注點是如何縮短垃圾收集時使用者執行緒的停頓時間。
  • G1 收集器:面向服務端應用。並行與併發、分代收集、空間整合、可預測停頓時間。

5、記憶體分配與回收策略

  • 物件優先在 Eden 分配。
  • 大物件直接進入老年代。
  • 長期存活的物件進入老年代。
  • 動態物件年齡判定。
  • 空間分配擔保。

三、虛擬機器效能監控與故障處理工具

1、JDK 的命令列工具

  • jps:顯示正在執行的虛擬機器程序。常用命令:jps -l
  • jstat:監視虛擬機器各種執行狀態資訊。常用命令:jstat -gcutil <pid>
  • jinfo:顯示虛擬機器配置資訊。常用命令:jinfo -flags <pid>
  • jmap:主要用於生成堆轉儲快照。常用命令:jmap -dump:format=b,file=<filename> <pid>
  • jhat:分析 jmap 生成的堆轉儲快照。常用命令:jhat <filename>
  • jstack:顯示虛擬機器當前時刻的執行緒堆疊資訊。常用命令:jstack -l <pid>

2、JDK 的視覺化工具

  • JConsole:Java 監視與管理控制檯
  • VisualVM:多合一故障處理工具

四、類檔案結構

1、無關性的基石

  • 各種不同平臺的虛擬機器。
  • 所有平臺都統一使用的位元組碼儲存格式。

2、Class 類檔案的結構

(1)Class 檔案的資料型別

  • 無符號數:基本資料型別,以 u1、u2、u4、u8 來分別代表 1 個位元組、2 個位元組、4 個位元組和 8 個位元組的無符號數。用於描述數字、索引引用、數量值或按照 UTF-8 編碼構成字串值。
  • 表:由多個無符號數或其他表作為資料項構成的複合資料型別,所有表都習慣性地以“_info”結尾。用於描述有層次關係的複合結構資料,整個 Class 檔案本質上就是一張表。

(2)Class 檔案格式

型別 名稱 數量
u4 magic(魔數) 1
u2 minor_version(次版本號) 1
u2 major_version(主版本號) 1
u2 constant_pool_count(常量池容量計數器) 1
cp_info constant_pool(常量池) constant_pool_count - 1
u2 access_flags(訪問標誌) 1
u2 this_class(類索引) 1
u2 super_class(父類索引) 1
u2 interfaces_count(介面計數器) 1
u2 interfaces(介面索引集合) interfaces_count
u2 fields_count(欄位表計數器) 1
field_info fields(欄位表集合) fields_count
u2 methods_count(方法表計數器) 1
method_info methods(方法表集合) methods_count
u2 attributes_count(屬性表計數器) 1
attribute_info attributes(屬性表集合) attributes_count
  • 魔數:Class 檔案的頭 4 個位元組,用於確定該檔案是否為 Class 檔案。其值為:0xCAFEBABE(咖啡寶貝?)。
  • Class 檔案的版本:第 5、6 個位元組是次版本號,第 7、8 個位元組是主版本號。
  • 常量池:可以理解為 Class 檔案中的資源倉庫。主要存放字面量和符號引用。每一項常量都是一個表。
  • 訪問標誌:用於識別一些類或介面層次的訪問資訊,包括:這個 Class 是類還是介面、是否定義為 public、是否定義為 abstract、是否宣告為 final(只有類可設定)等。
  • 類索引、父類索引與介面索引集合:Class 檔案由這三項資料確定這個類的繼承關係。
  • 欄位表集合:用於描述介面或類中宣告的變數。包括類變數和例項變數,但不包括在方法內部宣告的區域性變數。
  • 方法表集合:用於描述介面或類中宣告的方法。
  • 屬性表集合:在 Class 檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。

3、位元組碼指令簡介

  • 載入和儲存指令:用於將資料在棧幀中的區域性變量表和運算元棧之間來回傳輸。
  • 運算指令:用於對兩個運算元以上的值進行某種特定運算,並把結果重新存入到運算元棧頂。
  • 型別轉換指令:將兩種不同的數值型別進行相互轉換。
  • 物件建立與訪問指令。
  • 運算元棧管理指令:用於直接操作運算元棧。
  • 控制轉移指令:讓 Java 虛擬機器有條件或無條件地從指定位置的指令繼續執行程式,而不是從控制轉移指令的下一條指令繼續執行程式。可認為控制轉移指令就是在有條件或無條件地修改 PC 暫存器的值。
  • 方法呼叫和返回指令。
  • 異常處理指令。
  • 同步指令:支援方法級的同步和方法內部一段指令序列的同步。

五、虛擬機器類載入機制

1、類載入的過程

載入 -> 連線(驗證、準備、解析) -> 初始化。

  • 載入:獲取二進位制位元組流,並在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這個類的各種資料的訪問入口。
  • 驗證:確保 Class 檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
    • 檔案格式驗證:驗證位元組流是否符合 Class 檔案格式的規範,並且能被當前版本的虛擬機器處理。
    • 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 Java 語言規範的要求。
    • 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
    • 符號引用驗證:對符合引用進行匹配性校驗,確保解析動作能正常執行。
  • 準備:為類變數分配記憶體並設定初始值。
  • 解析:將常量池內的符號引用替換為直接引用。
  • 初始化:根據程式設計師的主觀計劃去初始化類變數和其他資源。

2、類載入器

  • 啟動類載入器(Bootstrap ClassLoader):負責將存放在 <JAVA_HOME>\lib 目錄的,或者 -Xbootclasspath 引數所指定路徑中的,能被虛擬機器識別的類庫載入到虛擬機器記憶體中。
  • 擴充套件類載入器(Extension ClassLoader):負責載入 <JAVA_HOME>\lib\ext 目錄中的,或者 java.ext.dirs 系統變數所指定路徑中的所有類庫。
  • 應用程式類載入器(Application ClassLoader):負責載入使用者類路徑上所指定的類庫。

3、雙親委派模型

如果一個類載入器收到類載入的請求,它會先把這個請求委派給父載入器去完成,而不會自己去嘗試載入這個類。只有父載入器無法完成這個載入請求時,子載入器才會嘗試自己去載入。

六、虛擬機器位元組碼執行引擎

1、執行時棧幀結構

棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構。棧幀儲存了方法的區域性變量表、運算元棧、動態連線、方法返回地址和一些額外的附加資訊。每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器裡面從入棧到出棧的過程。

  • 區域性變量表:是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。
  • 運算元棧:也稱為操作棧,它是一個後入先出的棧。運算元棧的每一個元素可以是任意的 Java 資料型別。
  • 動態連線:每個棧幀都包含一個指向執行時常量池中,該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。
  • 方法返回地址:方法退出後需要返回到方法被呼叫的位置,程式才能繼續執行。
  • 附加資訊:虛擬機器規範允許具體的虛擬機器實現增加一些規範裡沒有描述的資訊到棧幀中,例如與除錯相關的資訊。

2、方法呼叫

方法呼叫並不等於方法執行,方法呼叫階段唯一的任務就是確定被呼叫方法的版本(即呼叫哪一個方法)。此時,在 Class 檔案裡儲存的只是符號引用,而不是直接引用,只有在類載入期間,甚至是執行期間才能確定目標方法的直接引用。

  • 解析:在類載入的解析階段,將方法的符號引用轉化為直接引用,這類方法呼叫稱為解析。這種解析能成立的前提是:方法在程式執行之前有一個可確定的呼叫版本,並且這個方法的呼叫版本在執行期不可改變,即“編譯期可知,執行期不可變”。
  • 分派:
    • 靜態分派:在編譯期依賴靜態型別(又稱外觀型別)來定位方法執行版本的分派動作,稱為靜態分派。靜態分派的典型應用是方法過載。
    • 動態分派:在執行期根據實際型別確定方法執行版本的分派過程,稱為動態分派。動態分派的典型應用是方法重寫。

七、早期(編譯期)優化

1、Javac 編譯過程

(1)解析與填充符號表

  • 詞法分析:將原始碼的字元流轉變為標記(Token)集合,標記是編譯過程的最小元素,關鍵字、變數名、字面量、運算子都可以成為標記。
  • 語法分析:根據 Token 序列構造抽象語法樹。
  • 填充符號表:符號表是由一組符號地址和符號資訊構成的表格,可以把它想象成雜湊表中 K-V 值對的形式。

(2)註解處理

在編譯期間對註解進行處理。可以讀取、修改、新增抽象語法樹中的任何元素。

(3)語義分析與位元組碼生成

  • 語義分析:對結構上正確的源程式進行上下文邏輯審查。
    • 標註檢查:包括變數使用前是否已被宣告、變數與賦值之間的資料型別是否能夠匹配等。
    • 資料及控制流分析:對程式上下文邏輯進行更進一步的驗證,包括區域性變數在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理等。
  • 解語法糖:虛擬機器執行時並不支援語法糖的語法,因此,需要在編譯階段還原回簡單的基礎語法結構。
  • 位元組碼生成:把前面各個步驟所生成的資訊(語法樹、符號表)轉化成位元組碼寫到磁碟中,同時還進行了少量的程式碼新增和轉換工作。

2、Java 語法糖

  • 泛型與型別擦除:泛型的本質是引數化型別的應用,即將所操作的資料型別指定為一個引數。
  • 自動裝箱與拆箱、遍歷迴圈、變長引數。
  • 條件編譯:編譯器在編譯時只對滿足條件的程式碼進行編譯,而將不滿足條件的程式碼捨棄。Java 語言可以使用條件為布林常量值的 if 語句進行條件編譯。

八、晚期(執行期)優化

1、HotSpot 虛擬機器內的即時編譯器

(1)直譯器與編譯器

  • 當程式需要迅速啟動和執行時,直譯器可以首先發揮作用,省去編譯的時間,立即執行。
  • 在程式執行後,隨著時間的推移,編譯器把越來越多的程式碼編譯成原生代碼後,可以獲取更高的執行效率。

(2)C1、C2 編譯器

  • C1 編譯器(Client Compiler):執行在 Client 模式。
  • C2 編譯器(Server Compiler):執行在 Server 模式。

(3)混合模式、解釋模式與編譯模式

  • 混合模式:直譯器與編譯器搭配使用的方式。
  • 解釋模式:全部程式碼都使用解釋方式執行,編譯器完全不介入工作。
  • 編譯模式:優先採用編譯方式執行,但是直譯器仍會在編譯無法進行時介入執行過程。

(4)分層編譯

分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次。

  • 第 0 層:程式解釋執行,直譯器不開啟效能監控功能,可觸發第 1 層編譯。
  • 第 1 層:也稱 C1 編譯,將位元組碼編譯為原生代碼,進行簡單、可靠的優化,必要時加入效能監控的邏輯。
  • 第 2 層(或 2 層以上):也稱 C2 編譯,也是將位元組碼編譯為原生代碼,但會啟用一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。

2、即時編譯觸發條件

(1)熱點程式碼

  • 被多次呼叫的方法。
  • 被多次執行的迴圈體。

(2)熱點探測

判斷一段程式碼是不是熱點程式碼,是不是需要觸發即時編譯,這樣的行為稱為熱點探測。

  • 基於取樣的熱點探測:虛擬機器週期性地檢查各個執行緒的棧頂,如果發現某個方法經常出現在棧頂,那這個方法就是“熱點程式碼”。
  • 基於計數器的熱點探測:虛擬機器為每個方法(甚至是程式碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定閾值就認為它是“熱點程式碼”。

HotSpot 虛擬機器使用的是基於計數器的熱點探測方法,它為每個方法準備了兩類計數器。

  • 方法呼叫計數器:統計方法被呼叫的次數。
  • 回邊計數器:統計一個方法中迴圈體程式碼執行的次數。

3、編譯優化技術

  • 公共子表示式消除:如果一個表示式 E 已經計算過了,並且從先前計算到現在 E 中所有變數的值都沒有變化,那麼 E 的這次出現就成了公共子表示式。對於這種表示式,沒有必要再次進行計算,直接用前面計算過的表示式結果代替 E 即可。
  • 陣列邊界檢查消除:編譯器通過資料流分析判定陣列下標是否會越界,如果分析後確定不會越界,那麼可以把陣列的上下界檢查消除。
  • 方法內聯:把目標方法的程式碼“複製”到發起呼叫的方法之中,避免發生真實的方法呼叫。
  • 逃逸分析:當一個物件在方法中定義後,如果它被外部方法所引用或被外部執行緒訪問到,那麼就說這個物件發生了逃逸。如果一個物件不會逃逸到方法或執行緒之外,那麼可以為這個變數進行一些高效的優化,比如棧上分配、同步消除、標量替換等。

九、Java 記憶體模型與執行緒

1、Java 記憶體模型

(1)主記憶體與工作記憶體

  • 所有的變數都儲存在主記憶體中。每條執行緒有自己的工作記憶體,工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝。
  • 執行緒對變數的操作必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
  • 不同的執行緒之間無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來完成。

(2)記憶體間互動操作

  • lock(鎖定):把一個主記憶體變數標識為一條執行緒獨佔的狀態。
  • unlock(解鎖):把一個處於鎖定狀態的主記憶體變數釋放出來。
  • read(讀取):把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的 load 動作使用。
  • load(載入):把 read 操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  • use(使用):把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store(儲存):把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的 write 操作使用。
  • write(寫入):把 store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

(3)volatile 的作用

  • 保證變數對所有執行緒的可見性。
  • 禁止指令重排序優化。

(4)原子性、可見性與有序性

  • 原子性:
    • 基本資料型別的訪問讀寫具備原子性: Java 記憶體模型直接保證了 read、load、assign、use、store 和 write 操作的原子性。
    • synchronized 程式碼塊之間的操作具備原子性:底層通過 lock 和 unlock 操作實現。
  • 可見性:當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。Java 記憶體模型通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性。
  • 有序性:如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。前半句是指“執行緒內表現為序列的語義”,後半句是指“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象。

(5)先行發生原則

  • 程式次序規則:在一個執行緒內,按照程式程式碼順序,書寫在前的操作先行發生於書寫在後的操作。準確地說,是控制流順序而不是程式程式碼順序,因為要考慮分支、迴圈等結構。
  • 管程鎖定規則:一個 unlock 操作先行發生於後面(時間上的先後順序)對同一個鎖的 lock 操作。
  • volatile 變數規則:對一個 volatile 變數的寫操作先行發生於後面(時間上的先後順序)對這個變數的讀操作。
  • 執行緒啟動規則:Thread 物件的 start() 方法先行發生於此執行緒的每一個動作。
  • 執行緒終止規則:執行緒中的所有操作都先行發生於對此執行緒的終止檢測。
  • 執行緒中斷規則:對執行緒 interrupt() 方法的呼叫先行發生於被中斷執行緒檢測到中斷事件的發生。
  • 物件終結規則:一個物件的初始化完成(建構函式執行結束)先行發生於它的 finalize() 方法的開始。
  • 傳遞性:如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那麼可以得出操作 A 先行發生於操作 C。

2、Java 與執行緒

(1)執行緒的實現

  • 使用核心執行緒實現:核心執行緒就是直接由作業系統核心支援的執行緒。
  • 使用使用者執行緒實現:使用者執行緒完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒的存在。
  • 使用使用者執行緒加輕量級程序混合實現:使用者執行緒還是完全建立在使用者空間中,而作業系統提供支援的輕量級程序則作為使用者執行緒和核心執行緒之間的橋樑。

(2)Java 執行緒排程

  • 協同式執行緒排程:執行緒的執行時間由執行緒本身來控制,執行緒執行完之後,主動通知系統切換到另外一個執行緒上。
  • 搶佔式執行緒排程:每個執行緒由系統來分配執行時間,執行緒的切換不由執行緒本身來決定。

Java 使用的執行緒排程方式就是搶佔式排程。

(3)執行緒狀態

  • 新建(New):執行緒建立後尚未啟動。
  • 執行(Runable):包括了作業系統執行緒狀態中的 Running 和 Ready,處於此狀態的執行緒有可能正在執行,也有可能正在等待著 CPU 為它分配執行時間。
  • 無限期等待(Waiting):不會被分配 CPU 執行時間,等待著被其他執行緒顯式地喚醒。
  • 限期等待(Timed Waiting):不會被分配 CPU 執行時間,無須等待被其他執行緒顯式地喚醒,在一定時間之後會由系統自動喚醒。
  • 阻塞(Blocked):執行緒被阻塞了,在等待著獲取到一個排他鎖。在程式等待進入同步區域的時候,執行緒將進入這種狀態。
  • 結束(Terminated):已終止執行緒的執行緒狀態,執行緒已經結束執行。

十、執行緒安全與鎖優化

1、Java 語言中的執行緒安全

按執行緒安全的“安全程度”由強至弱排序,可以將多個執行緒的共享資料分為 5 類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立。

  • 不可變:不可變的物件一定是執行緒安全的,無論是物件的方法實現還是方法的呼叫者,都不需要再採取任何的執行緒安全保障措施。
  • 絕對執行緒安全:必須滿足“不管執行時環境如何,呼叫者都不需要任何額外的同步措施”。
  • 相對執行緒安全:就是我們通常意義上所講的執行緒安全,它需要保證對一個物件單獨的操作是執行緒安全的,但是對於一些特定順序的連續呼叫,則需要在呼叫端使用額外的同步手段來保證呼叫的正確性。
  • 執行緒相容:物件本身並不是執行緒安全的,但可以通過在呼叫端正確地使用同步手段來保證物件在併發環境中可以安全地使用。
  • 執行緒對立:無論呼叫端是否採取了同步措施,都無法在多執行緒環境中併發使用的程式碼。

2、執行緒安全的實現方法

  • 互斥同步(阻塞同步):同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個執行緒使用,而互斥是實現同步的一種手段。
  • 非阻塞同步:在進行同步操作時,不需要把執行緒掛起,而是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了衝突,那就採取其他的補償措施。
  • 無同步方案:
    • 可重入程式碼(純程式碼):如果一個方法的返回結果是可以預測的,只要輸入了相同的資料,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是執行緒安全的。
    • 執行緒本地儲存:如果能保證使用共享資料的程式碼在同一個執行緒中執行,那麼就可以把共享資料的可見範圍限制在同一個執行緒之內。這樣,無須同步也能保證執行緒之間不出現資料爭用的問題。

3、鎖優化

  • 自旋鎖:如果物理機有多個處理器,能讓多個執行緒同時並行執行,那麼可以讓後面請求鎖的執行緒“稍等一下”,但不放棄處理器的執行時間,然後看看持有鎖的執行緒是否很快就會釋放鎖。為了讓執行緒等待,只需讓執行緒執行一個忙迴圈(自旋),這就是所謂的自旋鎖。
  • 鎖消除:鎖消除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除。
  • 鎖粗化:如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作是出現在迴圈體中的,那麼虛擬機器將會把加鎖同步的範圍擴充套件(粗化)到整個操作序列的外部,這樣只需要加鎖一次就可以了。
  • 輕量級鎖:輕量級鎖並不是用來代替重量級鎖的,而是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。物件頭的 Mark Word 有個鎖標誌位,用於標識同步物件的鎖狀態。
  • 偏向鎖:偏向鎖是指這個鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其他執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

相關文章

《深入理解 Java 虛擬機器》讀書筆記:Java 記憶體區域與記憶體溢位異常
《深入理解 Java 虛擬機器》讀書筆記:垃圾收集器與記憶體分配策略
《深入理解 Java 虛擬機器》讀書筆記:虛擬機器效能監控與故障處理工具
《深入理解 Java 虛擬機器》讀書筆記:類檔案結構
《深入理解 Java 虛擬機器》讀書筆記:虛擬機器類載入機制
《深入理解 Java 虛擬機器》讀書筆記:虛擬機器位元組碼執行引擎
《深入理解 Java 虛擬機器》讀書筆記:早期(編譯期)優化
《深入理解 Java 虛擬機器》讀書筆記:晚期(執行期)優化
《深入理解 Java 虛擬機器》讀書筆記:Java 記憶體模型與執行緒
《深入理解 Java 虛擬機器》讀書筆記:執行緒安全與鎖優化

交流區


微信公眾號:驚卻一目
個人部落格:驚卻一