p32 面試題2:實現Singleton 模式面試
/**
* 多線程高效單例 Double Check
* @Author: hebin.yang
* @CreateDate: 2019-06-30 11:35
*/
public class Ex2Singleton3 {
private static Ex2Singleton3 instance;
private Ex2Singleton3() {}
/**
* Double Check
* 分析:因爲 synchronized 關鍵字能夠確保在釋放鎖以前,將數據刷新到主內存中,線程2 能夠讀取到最新的值
* 從而確保不會重複建立對象。
* 缺陷:無序性
* new 一個對象分兩步,並且是無序的。
* @return
*/
public static Ex2Singleton3 getInstance() {
if (instance == null) {
synchronized (Ex2Singleton3.class) {
if (instance == null) {
instance = new Ex2Singleton3();
}
}
}
return instance;
}
}
複製代碼
/**
* 安全版 Double Check 單例模式
* @Author: hebin.yang
* @CreateDate: 2019-06-30 16:02
*/
public class Ex2Singleton4 {
/**
* 增長 volatile 修飾,禁止建立對象指令重排序,修復多線程下因爲指令重排序致使的異常
*/
private static volatile Ex2Singleton4 instance;
private Ex2Singleton4() {}
public static Ex2Singleton4 getInstance() {
if (instance == null) {
synchronized (Ex2Singleton4.class) {
if (instance == null) {
instance = new Ex2Singleton4();
}
}
}
return instance;
}
}
複製代碼
關於 volatile 指令重排序的分析,詳見如下文章:安全
Double check 爲什麼須要 volatile?bash
劍指Offer 說 double check 方法有點複雜容易出錯實在有點牽強。多線程
特性:利用在 Java 中,靜態內部類只會在第一次調用到時進行初始化的特性,實現單例模式。post
/**
* 靜態內部類單例模式
* @Author: hebin.yang
* @CreateDate: 2019-06-30 16:33
*/
public class Ex2Singleton5 {
static class SingletonHolder {
private static Ex2Singleton5 INSTANCE = new Ex2Singleton5();
}
private Ex2Singleton5() {}
public static Ex2Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
}
複製代碼
分析:當Ex2Singleton5 加載時,並不會實例化靜態內部類,只有當第一次調用靜態內部類的 INSTANCE 時,纔會加載 SingletonHolder,從而實例化 Ex2Singleton5 這個單例的對象。性能
優點:因爲這種場景下單例對象的實例化,是在完成靜態內部類加載時完成的,因此天生對多線程友好。getInstance() 也不須要關鍵字來同步。ui
優勢:延遲加載,無需增長同步關鍵字(減小性能損耗)。spa
原理線程
因爲靜態內部類加載時只會執行一次類初始化方法 <clinit>()
,JVM 對該方法加了鎖,確保當多個線程同時試圖去執行類的初始化方法 <clinit>()
時,只有一個線程能執行,其他線程阻塞。而且,確保該方法只被執行一次。code
因此實現了線程安全。
總結:一個靜態內部類被加載初始化時,從類加載器的角度來看,是線程安全的,而單例對象 INSTANCE 在這個靜態內部類中是一個靜態 Field,因此它跟隨這個靜態內部類的加載而初始化,且只在類加載時初始化一次。
詳細分析可參考:《類加載時機彙總》