如何用JAVA語言分析雙重檢查鎖定

1、雙重檢查鎖定

在程序開發中,有時需要推遲一些高開銷的對象初始化操作,并且只有在使用這些對象時才進行初始化,此時可以采用雙重檢查鎖定來延遲對象初始化操作。雙重檢查鎖定是設計用來減少并發系統中競爭和同步開銷的一種軟件設計模式,在普通單例模式的基礎上,先判斷對象是否已經被初始化,再決定要不要加鎖。盡管雙重檢查鎖定解決了普通單例模式的在線程環境中易出錯和線程不安全的問題,但仍然存在一些隱患。下面以Java語言源代碼為例,分析雙重檢查鎖定缺陷產生的原因以及修復方法。

2、?雙重檢查鎖定的危害

雙重檢查鎖定在單線程環境中并無影響,在多線程環境下,由于線程隨時會相互切換執行,在指令重排的情況下,對象未實例化完全,導致程序調用出錯。

3、示例代碼

示例源于Samate Juliet Test Suite for Java v1.3 (https://samate.nist.gov/SARD/testsuite.php),源文件名:CWE609_Double_Checked_Locking__Servlet_01.java。

3.1缺陷代碼

如何用JAVA語言分析雙重檢查鎖定

上述代碼行23行-38行,程序先判斷 StringBad 是否為 NULL,如果不是則直接返回該 String 對象,這樣避免了進入 synchronized 塊所需要花費的資源。當 stringBad 為 null 時,使用 synchronized 關鍵字在多線程環境中避免多次創建 String 對象。在代碼實際運行時,以上代碼仍然可能發生錯誤。

立即學習Java免費學習筆記(深入)”;

對于第33行,創建 stringBad 對象和賦值操作是分兩步執行的。但 jvm 不保證這兩個操作的先后順序。當指令重排序后,JVM 會先賦值指向了內存地址,然后再初始化 stringBad 對象。如果此時存在兩個線程,兩個線程同時進入了第27行。線程1首先進入了 synchronized 塊,由于 stringBad 為 null,所以它執行了第33行。當 JVM 對指令進行了重排序,JVM 先分配了實例的空白內存,并賦值給 stringBad,但這時 stringBad 對象還未實例化,然后線程1離開了 synchronized 塊。當線程2進入 synchronized 塊時,由于 stringBad 此時不是 null ,直接返回了未被實例化的對象(僅有內存地址值,對象實際未初始化)。后續線程2調用程序對 stringBad 對象進行操作時,此時的對象未被初始化,于是錯誤發生。

使用360代碼衛士對上述示例代碼進行檢測,可以檢出“雙重檢查鎖定”缺陷,顯示等級為中。在代碼行第27行報出缺陷,如圖1所示:

如何用JAVA語言分析雙重檢查鎖定

圖1:“雙重檢查鎖定”的檢測示例

3.2 修復代碼

如何用JAVA語言分析雙重檢查鎖定

在上述修復代碼中,在第23行使用 volatile 關鍵字來對單例變量 stringBad 進行修飾。 volatile 作為指令關鍵字確保指令不會因編譯器的優化而省略,且要求每次直接讀值。

由于編譯器優化,代碼在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程序執行結果與源代碼相同,卻不保證實際指令的順序與源代碼相同,在單線程環境中并不會出錯,然而一旦引入多線程環境,這種亂序就可能導致嚴重問題。 volatile 關鍵字就可以從語義上解決這個問題,值得關注的是 volatile 的禁止指令重排序優化功能在 Java 1.5 后才得以實現,因此1.5 前的版本仍然是不安全的,即使使用了 volatile 關鍵字。

使用360代碼衛士對修復后的代碼進行檢測,可以看到已不存在“雙重檢查鎖定”缺陷。如圖2:

如何用JAVA語言分析雙重檢查鎖定

圖2:修復后檢測結果

4?、如何避免雙重檢查鎖定

要避免雙重檢查鎖定,需要注意以下幾點:

(1)使用 volatile?關鍵字避免指令重排序,但這個解決方案需要 JDK5 或更高版本,因為從JDK5 開始使用新的 JSR-133 內存模型規范,這個規范增強了 volatile?的語義。

(2)基于類初始化的解決方案。

如何用JAVA語言分析雙重檢查鎖定JVM在類的初始化階段(即在class被加載后,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。

? 版權聲明
THE END
喜歡就支持一下吧
點贊10 分享