天天看點

美團二面之高手過招:Java裡面為什麼搞了雙重檢查鎖

作者:網際網路技術學堂

引言

Java中的雙重檢查鎖(Double Checked Locking)是一種在多線程環境下保證線程安全的常用設計模式。這個模式的主要思想是先進行一次空的非同步檢查,然後再進入同步塊進行進一步的檢查,進而避免多個線程同時進入同步塊的情況,提高了代碼的性能。然而,雙重檢查鎖在Java中的實作卻十分複雜,容易出現線程安全問題,是以本篇部落格将對雙重檢查鎖的實作原理和問題進行深入分析,并給出一些案例和代碼。

美團二面之高手過招:Java裡面為什麼搞了雙重檢查鎖

第一章:基本概念

在介紹雙重檢查鎖的實作原理之前,我們先來了解一下幾個基本概念:

  1. 線程安全:多個線程同時通路一個共享資源時,不會出現資料不一緻或程式異常的情況,即程式的行為和預期一緻,就稱為線程安全。
  2. 同步鎖:是一種用于控制多個線程通路共享資源的機制。當一個線程通路某個共享資源時,需要先擷取該資源的同步鎖,其他線程則無法擷取該資源的同步鎖,進而避免了多個線程同時通路該資源的情況。
  3. 互斥鎖:是一種特殊的同步鎖,用于控制多個線程通路共享資源的互斥性。當一個線程擷取了某個共享資源的互斥鎖時,其他線程無法擷取該資源的互斥鎖,進而避免了多個線程同時通路該資源的情況。

第二章:雙重檢查鎖的實作原理

雙重檢查鎖是一種用于實作延遲初始化(Lazy Initialization)的機制。延遲初始化指的是在第一次使用某個對象時才進行初始化,進而避免了在程式啟動時就進行對象的建立,提高了代碼的性能。下面是雙重檢查鎖的實作代碼:

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

在上面的代碼中,我們首先對instance變量進行了一次非同步檢查,這是為了避免每次調用getInstance方法都需要進行同步操作,進而提高了代碼的性能。如果instance變量不為null,則直接傳回instance對象,否則我們進入同步塊進行進一步的檢查。在同步塊中我們再次對instance變量進行檢查,如果instance變量仍然為null,則進行對象的建立并将其指派給instance變量。

其中,關鍵字volatile的作用是保證instance變量在多線程環境下的可見性,即任何線程對該變量的修改都會立即反映到其他線程中,進而避免出現資料不一緻的情況。另外,使用synchronized關鍵字對getInstance方法進行同步操作,保證了在同一時刻隻有一個線程可以進入同步塊中進行操作,進而避免了多個線程同時通路instance變量的情況。

美團二面之高手過招:Java裡面為什麼搞了雙重檢查鎖

第三章:雙重檢查鎖的問題

盡管雙重檢查鎖可以有效地保證線程安全,但是其實作過程中仍然存在一些問題,這些問題主要包括以下幾個方面:

  1. 線程安全性問題:雖然雙重檢查鎖通過兩次檢查的方式保證了線程安全,但是如果在對象的構造函數中進行了耗時的操作,可能會導緻線程安全性問題。
  2. 可見性問題:如果在初始化對象的過程中,出現了指令重排的情況,可能會導緻對象的初始化順序發生變化,進而引發線程安全性問題。
  3. 可重入性問題:如果在初始化對象的過程中,另一個線程調用了getInstance方法,可能會導緻對象被建立多次,進而引發可重入性問題。
  4. 性能問題:每次調用getInstance方法時都需要進行非同步檢查,這會對程式的性能産生影響。

第四章:解決方案

為了解決雙重檢查鎖存在的問題,我們可以采用以下幾種解決方案:

  • 使用靜态内部類:靜态内部類在外部類被加載時不會被初始化,隻有在第一次調用getInstance方法時才會進行初始化,進而實作了延遲初始化的效果,并且由于靜态内部類隻會被初始化一次,是以不會存線上程安全性問題。
public class Singleton {
private Singleton() {}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}           
  • 使用枚舉類:枚舉類在Java中是天然的單例模式,且由于枚舉類在程式啟動時就已經被加載,是以不存線上程安全性問題。
public enum Singleton {
INSTANCE;
}           
  • 使用懶漢式單例模式:懶漢式單例模式在對象被第一次使用時才進行初始化,可以實作延遲初始化的效果,并且由于隻有在第一次調用getInstance方法時才會進行同步操作,是以不會存線上程安全性問題。
public class Singleton {
private static Singleton INSTANCE;

private Singleton() {}

public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}           

需要注意的是,雖然以上三種解決方案都可以有效地避免雙重檢查鎖存在的問題,但是枚舉類和靜态内部類在實作單例模式時更為簡潔和安全,是以建議優先考慮使用枚舉類或者靜态内部類來實作單例模式。

美團二面之高手過招:Java裡面為什麼搞了雙重檢查鎖

第五章:案例分析

下面我們通過一個實際的案例來示範如何使用雙重檢查鎖實作單例模式。

public class DatabaseConnection {
private static volatile DatabaseConnection INSTANCE;

private DatabaseConnection() {}

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

在上述代碼中,我們通過一個DatabaseConnection類來實作資料庫連接配接的單例模式。該類的構造函數是私有的,隻有getInstance方法可以建立對象,而getInstance方法通過雙重檢查鎖的方式來保證線程安全性。此外,我們還使用了volatile關鍵字來保證instance變量的可見性,以及synchronized關鍵字來保證getInstance方法的同步性。

第六章:總結

本文主要講解了Java中如何使用雙重檢查鎖來實作單例模式,并且深入分析了雙重檢查鎖存在的問題和解決方案。在實際開發中,我們建議優先考慮使用靜态内部類或者枚舉類來實作單例模式,以避免雙重檢查鎖存在的問題。

繼續閱讀