1. 程式人生 > >為什麼雙重檢查鎖模式需要 volatile ?

為什麼雙重檢查鎖模式需要 volatile ?

雙重檢查鎖定(Double check locked)模式經常會出現在一些框架原始碼中,目的是為了延遲初始化變數。這個模式還可以用來建立單例。下面來看一個 Spring 中雙重檢查鎖定的例子。

這個例子中需要將配置檔案載入到 handlerMappings中,由於讀取資源比較耗時,所以將動作放到真正需要 handlerMappings 的時候。我們可以看到 handlerMappings 前面使用了volatile 。有沒有想過為什麼一定需要 volatile?雖然之前瞭解了雙重檢查鎖定模式的原理,但是卻忽略變數使用了 volatile

下面我們就來看下這背後的原因。

錯誤的延遲初始化例子

想到延遲初始化一個變數,最簡單的例子就是取出變數進行判斷。

這個例子在單執行緒環境交易正常執行,但是在多執行緒環境就有可能會丟擲空指標異常。為了防止這種情況,我們需要使用 synchronized 。這樣該方法在多執行緒環境就是安全的,但是這麼做就會導致每次呼叫該方法獲取與釋放鎖,開銷很大。

深入分析可以得知只有在初始化的變數的需要真正加鎖,一旦初始化之後,直接返回物件即可。

所以我們可以將該方法改造以下的樣子。

這個方法首先判斷變數是否被初始化,沒有被初始化,再去獲取鎖。獲取鎖之後,再次判斷變數是否被初始化。第二次判斷目的在於有可能其他執行緒獲取過鎖,已經初始化改變數。第二次檢查還未通過,才會真正初始化變數。

這個方法檢查判定兩次,並使用鎖,所以形象稱為雙重檢查鎖定模式。

這個方案縮小鎖的範圍,減少鎖的開銷,看起來很完美。然而這個方案有一些問題卻很容易被忽略。

new 例項背後的指令

這個被忽略的問題在於 Cache cache=new Cache() 這行程式碼並不是一個原子指令。使用 javap -c指令,可以快速檢視位元組碼。

    // 建立 Cache 物件例項,分配記憶體
       0: new           #5                  // class com/query/Cache
       // 複製棧頂地址,並再將其壓入棧頂
       3: dup
    // 呼叫構造器方法,初始化 Cache 物件
       4: invokespecial #6                  // Method "<init>":()V
    // 存入區域性方法變量表
       7: astore_1

從位元組碼可以看到建立一個物件例項,可以分為三步:

  1. 分配物件記憶體
  2. 呼叫構造器方法,執行初始化
  3. 將物件引用賦值給變數。

虛擬機器實際執行時,以上指令可能發生重排序。以上程式碼 2,3 可能發生重排序,但是並不會重排序 1 的順序。也就是說 1 這個指令都需要先執行,因為 2,3 指令需要依託 1 指令執行結果。

Java 語言規規定了執行緒執行程式時需要遵守 intra-thread semantics。intra-thread semantics 保證重排序不會改變單執行緒內的程式執行結果。這個重排序在沒有改變單執行緒程式的執行結果的前提下,可以提高程式的執行效能。

雖然重排序並不影響單執行緒內的執行結果,但是在多執行緒的環境就帶來一些問題。

上面錯誤雙重檢查鎖定的示例程式碼中,如果執行緒 1 獲取到鎖進入建立物件例項,這個時候發生了指令重排序。當執行緒1 執行到 t3 時刻,執行緒 2 剛好進入,由於此時物件已經不為 Null,所以執行緒 2 可以自由訪問該物件。然後該物件還未初始化,所以執行緒 2 訪問時將會發生異常。

volatile 作用

正確的雙重檢查鎖定模式需要需要使用 volatilevolatile主要包含兩個功能。

  1. 保證可見性。使用 volatile 定義的變數,將會保證對所有執行緒的可見性。
  2. 禁止指令重排序優化。

由於 volatile 禁止物件建立時指令之間重排序,所以其他執行緒不會訪問到一個未初始化的物件,從而保證安全性。

注意,volatile禁止指令重排序在 JDK 5 之後才被修復

使用區域性變數優化效能

重新檢視 Spring 中雙重檢查鎖定程式碼。

可以看到方法內部使用區域性變數,首先將例項變數值賦值給該區域性變數,然後再進行判斷。最後內容先寫入區域性變數,然後再將區域性變數賦值給例項變數。

使用區域性變數相對於不使用區域性變數,可以提高效能。主要是由於 volatile 變數建立物件時需要禁止指令重排序,這就需要一些額外的操作。

總結

物件的建立可能發生指令的重排序,使用 volatile 可以禁止指令的重排序,保證多執行緒環境內的系統安全。

幫助文件

雙重檢查鎖定與延遲初始化
有關“雙重檢查鎖定失效”的說明

相關推薦

為什麼雙重檢查模式需要 volatile

雙重檢查鎖定(Double check locked)模式經常會出現在一些框架原始碼中,目的是為了延遲初始化變數。這個模式還可以用來建立單例。下面來看一個 Spring 中雙重檢查鎖定的例子。 這個例子中需要將配置檔案載入到 handlerMappings中,由於讀取資源比較耗時,所以將動作放到真正需要

java單例雙重檢查為什麼需要volatile關鍵字

Re: 炸斯特 2015-09-04 10:49發表 [回覆] [引用] [舉報] 回覆qq_30486849:我的理解,volatile是要保證可見性,即instance例項化後馬上對其他執行緒可見,而synchronized能同時保證原子性和可見性,同一時刻只有一個執

雙重檢查實現單例模式的線程安全問題

多線程 urn blog 內存 http 代碼 地方 gets 技術博客 一、結論 雙重校驗鎖的單例模式代碼如下: public class Singleton {   private static Singleton singleton;   private Singl

傳統單例模式雙重檢查存在的問題

單例模式1.0: public class Singleton { private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) {  // 1 s

(GOF23設計模式)_單例模式_雙重檢查式_靜態內部類式_列舉式

設計模式 a、建立型模式 單例模式、工廠模式、抽象工廠模式、建造者模式、原型模式 b、結構型模式 介面卡模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式 c、行為型模式 模板方法模式、命令模式、迭代器模式、觀察者模式、中介

Java單例模式雙重檢查的問題

單例建立模式是一個通用的程式設計習語。和多執行緒一起使用時,必需使用某種型別的同步。在努力建立更有效的程式碼時,Java 程式設計師們建立了雙重檢查鎖定習語,將其和單例建立模式一起使用,從而限制同步程式碼量。然而,由於一些不太常見的 Java 記憶體模型細節的原因,並不能

設計模式(01) 單例模式(建立類模式)(下,懶漢模式雙重檢查

From Now On,Let us begin Design Patterns。 懶漢模式和雙重檢查鎖 這篇文章我們接著上一篇文章,繼續設計模式裡面的單例模式:這一篇我們要寫的是懶漢模式和雙重檢查加鎖的例項,我用我個人的程式設計經驗跟大家講述這個很有趣

單例模式中 的 雙重檢查 概念與用法

它的 lock acc env syn 可見 cost ola check public class Singleton { //私有的 靜態的 本類屬性 private volatile static Singleton _instance;

物件部分初始化:原理以及驗證程式碼(雙重檢查volatile相關)

# 物件部分初始化:原理以及驗證程式碼(雙重檢查鎖與volatile相關) 物件部分初始化被稱為 *[Partially initialized objects](https://wiki.sei.cmu.edu/confluence/display/java/TSM03-J.+Do+not+publish

關於並發場景下,通過雙重檢查實現延遲初始化的優化問題隱患的記錄

ron href 修飾符 屬性 tin 記錄 targe turn 優化問題   首先,這個問題是從《阿裏巴巴Java開發手冊》的1.6.12(P31)上面看到的,裏面有這樣一句話,並列出一種反例代碼(以下為仿寫,並非與書上一致):   在並發場景下,通過雙重檢查鎖(do

雙重檢查實現單例(java)

urn rdl == ini var import 如何 安全 why 單例類在Java開發者中非常常用,但是它給初級開發者們造成了很多挑戰。他們所面對的其中一個關鍵挑戰是,怎樣確保單例類的行為是單例?也就是說,無論任何原因,如何防止單例類有多個實例。在整個應用生命周期中,

C++和雙重檢查鎖定模式(DCLP)的風險

原文連結 多執行緒其實就是指兩個任務一前一後或者同時發生。 1 簡介 當你在網上搜索設計模式的相關資料時,你一定會找到最常被提及的一個模式:單例模式(Singleton)。然而,當你嘗試在專案中使用單例模式時,一定會遇到一個很重要的限制:若使用傳統的實現方法(我們會在下

java-雙重檢查為什麼多執行緒不安全

如下程式碼所示: public class doubleCheck{ private static Instance instance; public static Instance getInstance(){ if(instance==null){ //1

雙重檢查機制

背景:我們在實現單例模式的時候往往會忽略掉多執行緒的情況,就是寫的程式碼在單執行緒的情況下是沒問題的,但是一碰到多個執行緒的時候,由於程式碼沒寫好,就會引發很多問題,而且這些問題都是很隱蔽和很難排查的。 例子1:沒有volatile修飾的uniqueInstance public class Singl

為什麼在單例類中不能使用雙重檢查來初始化物件

在網上看到過好多篇文章在說明雙重檢查鎖在多個執行緒初始化一個單例類時到底為什麼不行時在關鍵位置的描述模稜兩可,今天我們就來看一下為什麼不能用雙重檢查鎖,問題到底出在了那裡? 下面我們直接進入主題,為什麼使用雙重檢查鎖,原因是因為在多執行緒初始化一個單例類時我們要確保得到一

單例陷阱——雙重檢查中的指令重排問題

之前我曾經寫過一篇文章《單例模式有8種寫法,你知道麼?》,其中提到了一種實現單例的方法-雙重檢查鎖,最近在讀併發方面的書籍,發現雙重檢查鎖使用不當也並非絕對安全,在這裡分享一下。 單例回顧 首先我們回顧一下最簡單的單例模式是怎樣的? /** *單例模式一:懶漢式(執行緒安全) */ public class

java單例模式雙重檢查)的原因

csharp sta get 第一次 instance new 同步機制 原因 AR public class Singleton{ private static Singleton instance = null;//是否是final的不重要,因為最多只可能實

單例模式中的雙重檢查

本文是在學習單例模式時遇到的問題 在多執行緒中,如何防止單例模式被多次例項,當然是要加鎖啦。但是加了鎖就意味著執行緒雖然安全,但效率肯定會變低,這是,就出現了雙重檢查加鎖。但看到這段程式碼,我又有疑問了? public class Singleton { private vo

java單例模式雙重檢查鎖定的volatile的作用解析

volatile對singleton的建立過程的重要性:禁止指令重排序(有序性)例項化一個物件其實可以分為三個步驟:  (1)分配記憶體空間。  (2)初始化物件。  (3)將記憶體空間的地址賦值給對應的引用。但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:  (1)分配記憶體空間。

java單例模式優缺點(懶漢模式,餓漢模式雙重檢查模式

三種單例模式實際都是有運用的。 優點:延遲載入 缺點:不加同步的懶漢式是執行緒不安全的,加了synchronized之後就變成執行緒安全的了 public class Singleton { private static Singleton singleto