1. 程式人生 > >面試題剖析:單例設計模式執行緒安全問題

面試題剖析:單例設計模式執行緒安全問題

本文作者:黃海燕,叩丁狼高階講師。原創文章,轉載請註明出處。

1. volatile 關鍵字

1.1 volatile 關鍵字作用:

在百度百科擷取的描述如下:

叩丁狼教育.png

叩丁狼教育.png

說明volatile 關鍵字作用作用有兩點:

  1. 防止指令重排:規定了volatile 變數不能指令重排,必須先寫再讀。

  2. 記憶體可見:執行緒從記憶體中讀取volatile修飾的變數的資料,直接從主記憶體中獲取資料,不需要經過CPU快取,這樣使得多執行緒獲取的資料都是一致的。如圖所示:


    叩丁狼教育.png

    叩丁狼教育.png

1.2 volatile和synchronized的區別

volatile不能夠替代synchronized,原因有兩點:

1.對於多執行緒,不是一種互斥關係
2.不能保證變數狀態的“原子性操作”,所以volatile不能保證原子性問題

1.3解決單例設計模式執行緒安全問題

實現單例設計模式兩種

  1. 餓漢式(不存在原子性,是執行緒安全的)

實現1:

//餓漢式:很餓需要立馬建立物件
public class Singleton1 {
    //1.定義一個物件
    private static final Singleton1 instance = new Singleton1();
    //2.私有化構造器,避免外部類建立物件
    private Singleton1(){}
    //3.獲取物件的靜態方法
    public static Singleton1 getInstance(){
        return instance;
    }
}

實現2:列舉方式(最安全)

//餓漢式(列舉)
public enum EnumSingleton {
   INSTANCE;
}
  1. 懶漢式(懶載入):存在原子性問題,執行緒不安全
//懶漢式:很懶,使用物件的時候才建立物件,但是省資源
public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

①由於 instance = new Singleton2();存在原子性問題,所以我們應該用synchronized程式碼塊將其同步。這裡由於synchronized很耗資源,所以粒度越小越好,最好不要使用同步方法。

 public static Singleton2 getInstance() {
     if (instance == null) {
          synchronized (Singleton2.class) {
              instance = new Singleton2();
          }
     }
     return instance;
 }

②在多個執行緒的情況,可能存線上程1和執行緒2都已經執行了instance == null的判斷,可能執行緒1搶到了鎖執行緒2就阻塞在了同步程式碼塊入口,當執行緒1執行完畢釋放鎖,執行緒2拿到鎖的時候因為之前判斷instance == null為true就會建立物件,那麼此時就無法保證單例了,所以我們應該繼續在同步程式碼塊中再判斷一次instance == null。這樣的做法我們有個專業名詞,稱之為雙重檢查鎖定。

    public static Singleton2 getInstance() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

③instance = new Singleton2();這句程式碼存在指令重排問題,什麼意思?

一般的執行順序為:

1)給物件分配記憶體空間
2)初始化物件
3)變數instance 指向記憶體空間

在單執行緒中,由於步驟2)和步驟3)即使交換順序也不會影響最終效果,所以可能發生指令重排,順序為:

1)給物件分配記憶體空間
3)變數instance 指向記憶體空間
2)初始化物件

如果出現指令重排就會發生以下問題,如圖所示:

叩丁狼教育.png

叩丁狼教育.png

注意:由於執行緒2在外面的判斷就為false,沒有去執行需要競爭鎖的程式碼,所以沒有進入阻塞狀態,和執行緒1是並行狀態,導致訪問物件出現問題,所以為了避免這個問題,我們應該不讓指令重排發生,那麼使用volatile修飾物件,讓物件先寫再讀,固定物件的指令,避免指令重排。

最終執行緒安全的單例懶漢式程式碼如下:

public class Singleton2 {
    private static volatile Singleton2 instance;
    private Singleton2() {}
    public static Singleton2 getInstance() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

想獲取更多技術視訊,請前往叩丁狼官網:http://www.wolfcode.cn/all_article.html