Java內存模型之從JMM角度分析DCL

DCL,即Double Check Lock,中衛雙重檢查鎖定。其實DCL很多人在單例模式中用過,LZ面試人的時候也要他們寫過,但是有很多人都會寫錯。他們為什麼會寫錯呢?其錯誤根源在哪裡?有什麼解決方案?下面就隨LZ一起來分析

問題分析

我們先看單例模式裡面的懶漢式:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton;

我們都知道這種寫法是錯誤的,因為它無法保證線程的安全性。優化如下:

public class Singleton { private static Singleton singleton; private Singleton(){} public static synchronized Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }}

優化非常簡單,就是在getInstance方法上面做了同步,但是synchronized就會導致這個方法比較低效,導致程序性能下降,那麼怎麼解決呢?聰明的人們想到了雙重檢查 DCL:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 1 synchronized (Singleton.class){ // 2 if(singleton == null){ // 3 singleton = new Singleton(); // 4 } } } return singleton; }}

就如上面所示,這個代碼看起來很完美,理由如下:

  • 如果檢查第一個singleton不為null,則不需要執行下面的加鎖動作,極大提高了程序的性能;

  • 如果第一個singleton為null,即使有多個線程同一時間判斷,但是由於synchronized的存在,只會有一個線程能夠創建對象;

  • 當第一個獲取鎖的線程創建完成後singleton對象後,其他的在第二次判斷singleton一定不會為null,則直接返回已經創建好的singleton對象;

通過上面的分析,DCL看起確實是非常完美,但是可以明確地告訴你,這個錯誤的。上面的邏輯確實是沒有問題,分析也對,但是就是有問題,那麼問題出在哪裡呢?在回答這個問題之前,我們先來複習一下創建對象過程,實例化一個對象要分為三個步驟:

  1. 分配內存空間

  2. 初始化對象

  3. 將內存空間的地址賦值給對應的引用

但是由於重排序的緣故,步驟2、3可能會發生重排序,其過程如下:

  1. 分配內存空間

  2. 將內存空間的地址賦值給對應的引用

  3. 初始化對象

如果2、3發生了重排序就會導致第二個判斷會出錯,singleton != null,但是它其實僅僅只是一個地址而已,此時對象還沒有被初始化,所以return的singleton對象是一個沒有被初始化的對象,如下:

DCL,即Double Check Lock,中衛雙重檢查鎖定。其實DCL很多人在單例模式中用過,LZ面試人的時候也要他們寫過,但是有很多人都會寫錯。他們為什麼會寫錯呢?其錯誤根源在哪裡?有什麼解決方案?下面就隨LZ一起來分析

問題分析

我們先看單例模式裡面的懶漢式:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton;

我們都知道這種寫法是錯誤的,因為它無法保證線程的安全性。優化如下:

public class Singleton { private static Singleton singleton; private Singleton(){} public static synchronized Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }}

優化非常簡單,就是在getInstance方法上面做了同步,但是synchronized就會導致這個方法比較低效,導致程序性能下降,那麼怎麼解決呢?聰明的人們想到了雙重檢查 DCL:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 1 synchronized (Singleton.class){ // 2 if(singleton == null){ // 3 singleton = new Singleton(); // 4 } } } return singleton; }}

就如上面所示,這個代碼看起來很完美,理由如下:

  • 如果檢查第一個singleton不為null,則不需要執行下面的加鎖動作,極大提高了程序的性能;

  • 如果第一個singleton為null,即使有多個線程同一時間判斷,但是由於synchronized的存在,只會有一個線程能夠創建對象;

  • 當第一個獲取鎖的線程創建完成後singleton對象後,其他的在第二次判斷singleton一定不會為null,則直接返回已經創建好的singleton對象;

通過上面的分析,DCL看起確實是非常完美,但是可以明確地告訴你,這個錯誤的。上面的邏輯確實是沒有問題,分析也對,但是就是有問題,那麼問題出在哪裡呢?在回答這個問題之前,我們先來複習一下創建對象過程,實例化一個對象要分為三個步驟:

  1. 分配內存空間

  2. 初始化對象

  3. 將內存空間的地址賦值給對應的引用

但是由於重排序的緣故,步驟2、3可能會發生重排序,其過程如下:

  1. 分配內存空間

  2. 將內存空間的地址賦值給對應的引用

  3. 初始化對象

如果2、3發生了重排序就會導致第二個判斷會出錯,singleton != null,但是它其實僅僅只是一個地址而已,此時對象還沒有被初始化,所以return的singleton對象是一個沒有被初始化的對象,如下:

Java內存模型之從JMM角度分析DCL

按照上面圖例所示,線程B訪問的是一個沒有被初始化的singleton對象。

通過上面的闡述,我們可以判斷DCL的錯誤根源在於步驟4:

singleton = new Singleton();

知道問題根源所在,那麼怎麼解決呢?有兩個解決辦法:

  1. 不允許初始化階段步驟2 、3發生重排序。

  2. 允許初始化階段步驟2 、3發生重排序,但是不允許其他線程“看到”這個重排序。

解決方案

解決方案依據上面兩個解決辦法即可。

基於volatile解決方案

對於上面的DCL其實只需要做一點點修改即可:將變量singleton生命為volatile即可:

public class Singleton { //通過volatile關鍵字來確保安全 private volatile static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; }}

當singleton聲明為volatile後,步驟2、步驟3就不會被重排序了,也就可以解決上面那問題了。

基於類初始化的解決方案

該解決方案的根本就在於:利用classloder的機制來保證初始化instance時只有一個線程。JVM在類初始化階段會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化。

public class Singleton { private static class SingletonHolder{ public static Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.singleton; }}

這種解決方案的實質是:運行步驟2和步驟3重排序,但是不允許其他線程看見。

Java語言規定,對於每一個類或者接口C,都有一個唯一的初始化鎖LC與之相對應。從C到LC的映射,由JVM的具體實現去自由實現。JVM在類初始化階段期間會獲取這個初始化鎖,並且每一個線程至少獲取一次鎖來確保這個類已經被初始化過了。

DCL,即Double Check Lock,中衛雙重檢查鎖定。其實DCL很多人在單例模式中用過,LZ面試人的時候也要他們寫過,但是有很多人都會寫錯。他們為什麼會寫錯呢?其錯誤根源在哪裡?有什麼解決方案?下面就隨LZ一起來分析

問題分析

我們先看單例模式裡面的懶漢式:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton;

我們都知道這種寫法是錯誤的,因為它無法保證線程的安全性。優化如下:

public class Singleton { private static Singleton singleton; private Singleton(){} public static synchronized Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }}

優化非常簡單,就是在getInstance方法上面做了同步,但是synchronized就會導致這個方法比較低效,導致程序性能下降,那麼怎麼解決呢?聰明的人們想到了雙重檢查 DCL:

public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 1 synchronized (Singleton.class){ // 2 if(singleton == null){ // 3 singleton = new Singleton(); // 4 } } } return singleton; }}

就如上面所示,這個代碼看起來很完美,理由如下:

  • 如果檢查第一個singleton不為null,則不需要執行下面的加鎖動作,極大提高了程序的性能;

  • 如果第一個singleton為null,即使有多個線程同一時間判斷,但是由於synchronized的存在,只會有一個線程能夠創建對象;

  • 當第一個獲取鎖的線程創建完成後singleton對象後,其他的在第二次判斷singleton一定不會為null,則直接返回已經創建好的singleton對象;

通過上面的分析,DCL看起確實是非常完美,但是可以明確地告訴你,這個錯誤的。上面的邏輯確實是沒有問題,分析也對,但是就是有問題,那麼問題出在哪裡呢?在回答這個問題之前,我們先來複習一下創建對象過程,實例化一個對象要分為三個步驟:

  1. 分配內存空間

  2. 初始化對象

  3. 將內存空間的地址賦值給對應的引用

但是由於重排序的緣故,步驟2、3可能會發生重排序,其過程如下:

  1. 分配內存空間

  2. 將內存空間的地址賦值給對應的引用

  3. 初始化對象

如果2、3發生了重排序就會導致第二個判斷會出錯,singleton != null,但是它其實僅僅只是一個地址而已,此時對象還沒有被初始化,所以return的singleton對象是一個沒有被初始化的對象,如下:

Java內存模型之從JMM角度分析DCL

按照上面圖例所示,線程B訪問的是一個沒有被初始化的singleton對象。

通過上面的闡述,我們可以判斷DCL的錯誤根源在於步驟4:

singleton = new Singleton();

知道問題根源所在,那麼怎麼解決呢?有兩個解決辦法:

  1. 不允許初始化階段步驟2 、3發生重排序。

  2. 允許初始化階段步驟2 、3發生重排序,但是不允許其他線程“看到”這個重排序。

解決方案

解決方案依據上面兩個解決辦法即可。

基於volatile解決方案

對於上面的DCL其實只需要做一點點修改即可:將變量singleton生命為volatile即可:

public class Singleton { //通過volatile關鍵字來確保安全 private volatile static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; }}

當singleton聲明為volatile後,步驟2、步驟3就不會被重排序了,也就可以解決上面那問題了。

基於類初始化的解決方案

該解決方案的根本就在於:利用classloder的機制來保證初始化instance時只有一個線程。JVM在類初始化階段會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化。

public class Singleton { private static class SingletonHolder{ public static Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.singleton; }}

這種解決方案的實質是:運行步驟2和步驟3重排序,但是不允許其他線程看見。

Java語言規定,對於每一個類或者接口C,都有一個唯一的初始化鎖LC與之相對應。從C到LC的映射,由JVM的具體實現去自由實現。JVM在類初始化階段期間會獲取這個初始化鎖,並且每一個線程至少獲取一次鎖來確保這個類已經被初始化過了。

Java內存模型之從JMM角度分析DCL

一個程序員學習平臺分享給你們,讓你在實踐中積累經驗掌握原理。主要方向是JAVA工程師。如果你想拿高薪,想突破瓶頸,想跟別人競爭能取得優勢的,想進BAT但是有擔心面試不過的,可以加我的Java學習交流群:282711949。

注:加群要求

1、大學學習的是Java相關專業,畢業後面試受挫,找不到對口工作可以

2、在公司待久了,現在過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的

3、參加過線下培訓後,知識點掌握不夠深刻,就業困難,想繼續深造

4、已經在Java相關部門上班的在職人員,對自身職業規劃不清晰,混日子的

5、有一定的C語言基礎,接觸過java開發,想轉行的

小號勿擾,不喜勿加

參考資料:方騰飛:《Java併發編程的藝術》

相關推薦

推薦中...