單例模式還能這樣寫?讓Android原始碼教你結合場景使用單例模式
偉大的海賊王哥爾?D?羅杰曾經說過:“去看原始碼吧!我把世上的一切都放在了那里,”
眾所周知,單例模式常用的有DCL、靜態內部類、列舉等等,靜態內部類方法可以實作延遲加載還沒有執行緒安全問題,列舉方法可以從JVM層面防止反射破壞單例模式,實際上我們還可以使用final來實作單例模式,這個后面再說,而DCL(Double Check Lock)是一種執行緒安全、延遲加載、效率高的懶漢式單例模式,大概長下面這樣,
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;
}
}`
其實就是縮小了鎖的粒度來提高性能,然后為了防止指令重排序導致物件半初始化問題使用了volatile來修飾物件,但是我在看Android的WindowManagerGlobal原始碼的時候發現Google開發團隊是這樣實作單例模式的:
private static WindowManagerGlobal sDefaultWindowManager;
private WindowManagerGlobal() {
}
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
嗯?居然少了一層if判斷,這樣子在物件已經存在的時候還要去獲取鎖不是讓效率變低了嗎?連volatile也不加,難道他們不擔心會出現物件半初始化的問題嗎?
帶著上面兩個問題,我去看了看getInstance方法呼叫的地方,只有兩處,一處就在類里面,另一處在WindowManager的實作類WindowManagerImpl中,而他是這樣用的:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
新的問題產生了,為什么要用final修飾一個單例物件?我們知道final修飾物件的時候是可以保證物件不可變,但是單例的物件不是已經不可變了嗎?看過《java并發編程藝術》應該知道,final還有一個重要的語意,那就是可以添加記憶體屏障,
volatile也有記憶體屏障,難道這里是使用了final來代替volatile來實作單例模式嗎?
我們先來看一個普通的餓漢式單例:
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
很多人不理解這里的final作用到底是什么,好像加不加都一樣,其實這里的作用是通過final的記憶體屏障來保證物件在初始化未完成的時候就被其他執行緒獲取也就是上面提到的物件半初始化問題, 這是網上的一種錯誤觀點,
我認為這里的final加不加都行,因為static保證了物件是在類加載的“初始化”階段就完成創建了,
再來看看真正用final實作單例模式是什么樣子的:
public final class Instance {
private final int n;
public Instance(int n) {
this.n = n;
}
// Other fields and methods, all fields are final
}
class Helper {
private static Instance instance = null;
public static Instance getHelper() {
Instance tem = instance; // Only unsynchronized read of helper
if (tem== null) {
synchronized (Helper.class) {
tem = instance; // In synchronized block, so this is safe
if (tem == null) {
tem = new Instance(42);
instance = tem;
}
}
}
return tem;
}
}
這種寫法初始化的物件暫時存在一個區域變數 tem 中,在 tem 初始化完成并賦值給instance之前,其他執行緒來訪問instance都會是null,final的作用就是防止tem還沒初始化完成就賦值給了instance,想了解具體原理可以自行去看看final域的讀寫規則,
再對比一下WindowManagerGlobal的單例模式就知道其實這里并沒有用final代替volatile的作用,
但是我們可以發現要實作執行緒安全的單例模式,其實只要控制對于物件非同步訪問就可以了,比如上面final的實作方法,對于物件instance的非同步訪問只有把instance賦值給區域變數tem一個地方,然后把對于instance的賦值操作放進了同步代碼塊里面,
我們再看一次WindowManagerGlobal的單例模式:
private static WindowManagerGlobal sDefaultWindowManager;
private WindowManagerGlobal() {
}
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
應該很多人都能看出來,少了一層 if 判空其實就不會有指令重排序導致的物件半初始化的問題,也就不需要用volatile修飾物件了,究其原因,就是把物件的訪問都放進同步代碼塊中,就算發生了指令重排序,其他執行緒依舊需要獲取鎖,等獲取到時物件也已經初始化完成了,
安全問題解決了,我們再來看看效率問題,乍一看每次都要去獲取鎖,獲取不到又要一直等啊等的,肯定效率下降很多吧?
我對于這個問題的理解是這樣的:
首先這種寫法沒有用volatile修飾變數,不用每次都去主記憶體更新資料,提高了一部分效率,
其次就是使用場景,WindowManagerGlobal物件一般是在對window進行添加、更新、移除操作的時候使用的,還有就是可見性發生變化的時候,而這些操作很少同時執行,就算同時執行也一般發生在主執行緒,也就是說鎖競爭的情況非常少,而鎖競爭不激烈的時候synchronized的鎖就會是無鎖或者偏向鎖,所以在主執行緒獲取鎖的時候可以直接獲取鎖或者讓系統對比一下執行緒資訊再獲取,不會對效率有什么影響,所以這種方式下平時主執行緒獲取物件的效率會比使用volatile的方式要高,
至于在WindowManagerImpl用final修飾單例物件,我只能理解為防止被重新賦值,
如果你有其他想法,歡迎在評論區發表,
參考文章:
一種獨特的單例模式寫法,利用final語意實作
LCK10-J. Use a correct form of the double-checked locking idiom
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/293765.html
標籤:其他
