双重检查锁,原来是这样演变来的,你了解吗
最近在看Nacos的源代碼時,發現多處都使用了“雙重檢查鎖”的機制,算是非常好的實踐案例。這篇文章就著案例來分析一下雙重檢查鎖的使用以及優勢所在,目的就是讓你的代碼格調更加高一個層次。
同時,基于單例模式,講解一下雙重檢查鎖的演變過程。
Nacos中的雙重檢查鎖
在Nacos的InstancesChangeNotifier類中,有這樣一個方法:
private?final?Map<String,?ConcurrentHashSet<EventListener>>?listenerMap?=?new?ConcurrentHashMap<String,?ConcurrentHashSet<EventListener>>();private?final?Object?lock?=?new?Object();public?void?registerListener(String?groupName,?String?serviceName,?String?clusters,?EventListener?listener)?{String?key?=?ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName,?groupName),?clusters);ConcurrentHashSet<EventListener>?eventListeners?=?listenerMap.get(key);if?(eventListeners?==?null)?{synchronized?(lock)?{eventListeners?=?listenerMap.get(key);if?(eventListeners?==?null)?{eventListeners?=?new?ConcurrentHashSet<EventListener>();listenerMap.put(key,?eventListeners);}}}eventListeners.add(listener); }該方法的主要功能就是對監聽器事件進行注冊。其中注冊的事件都存在成員變量listenerMap當中。listenerMap的數據結構是key為String,value為ConcurrentHashSet的Map。也就是說,一個key對應一個集合。
針對這種數據結構,在多線程的情況下,Nacos處理流程如下:
通過key獲取value值;
判斷value是否為null;
如果value值不為null,則直接將值添加到Set當中;
如果為null,就需要創建一個ConcurrentHashSet,在多線程時,有可能會創建多個,因此要使用鎖。
通過synchronized鎖定一個Object對象;
在鎖內再獲取一次value值,如果依然是null,則進行創建。
進行后續操作。
上述過程,在鎖定前和鎖定之后,做了兩次判斷,因此稱作”雙重檢查鎖“。使用鎖的目的就是避免創建多個ConcurrentHashSet。
Nacos中的實例稍微復雜一下,下面以單例模式中的雙重檢查鎖的演變過程。
未加鎖的單例
這里直接演示單例模式的懶漢模式實現:
public?class?Singleton?{private?static?Singleton?instance;private?Singleton()?{}public?Singleton?getInstance()?{if?(instance?==?null)?{instance?=?new?Singleton();}return?instance;}???? }這是一個最簡單的單例模式,在單線程下運轉良好。但在多線程下會出現明顯的問題,可能會創建多個實例。
以兩個線程為例:
可以看到,當兩個線程同時執行時,是有可能會創建多個實例的,這很明顯不符合單例的要求。
加鎖單例
針對上述代碼的問題,很直觀的想到是進行加鎖處理,實現代碼如下:
public?class?Singleton?{private?static?Singleton?instance;private?Singleton()?{}public?synchronized?Singleton?getInstance()?{if?(instance?==?null)?{instance?=?new?Singleton();}return?instance;} }與第一個示例唯一的區別是在方法上添加了synchronized關鍵字。這時,當多個線程進入該方法時,需要先獲得鎖才能進行執行。
通過在方法上添加synchronized關鍵字,看似完美的解決了多線程的問題,但卻帶了性能問題。
我們知道使用鎖會導致額外的性能開銷,對于上面的單例模式,只有第一次創建時需要鎖(防止創建多個實例),但查詢時是不需要鎖的。
如果針對方法進行加鎖,每次查詢也要承擔加鎖的性能損耗。
雙重檢查鎖
針對上面的問題,就有了雙重檢查鎖,示例如下:
public?class?Singleton?{private?static?Singleton?instance;private?Singleton()?{}public?Singleton?getInstance()?{if?(instance?==?null)?{synchronized?(Singleton.class)?{if?(instance?==?null)?{instance?=?new?Singleton();}}}return?instance;} }第一,將鎖的范圍縮小的方法內;
第二,鎖之前先判斷一下是不是null,如果不為null,說明已經實例化了,直接返回,沒必要進行創建;
第三,如果為null,進行加鎖,然后再次判斷是否為null。為什么要再次判斷?因為一個線程判斷為null之后,另外一個線程可能已經創建了對象,所以在鎖定之后,需要再次核實一下,真的為null,則進行對象創建。
改進之后,既保證了線程的安全性,又避免了鎖導致的性能損失。問題到此結束了嗎?并沒有,繼續往下看。
JVM的指令重排
在某些JVM當中,編譯器為了性能問題,會進行指令重排。在上述代碼中new Singleton()并不是原子操作,有可能會被編譯器進行重排操作。
創建對象可抽象為三步:
memory = allocate();????//1:分配對象的內存空間? ctorInstance(memory);??//2:初始化對象? instance = memory;?????//3:設置instance指向剛分配的內存地址上面操作中,操作2依賴于操作1,但操作3并不依賴于操作2。因此,JVM是可以進行指令重排優化的,可能會出現如下的執行順序:
memory = allocate();????//1:分配對象的內存空間? instance = memory;?????//3:instance指向剛分配的內存地址,此時對象還未初始化 ctorInstance(memory);??//2:初始化對象指令重排之后,將操作3的賦值操作放在了前面,那就會出現一個問題:當線程A執行完步驟賦值操作,但還未執行對象初始化。此時,線程B進來了,在第一層判斷時發現Instance已經有值了(實際上還未初始化),直接返回對應的值。那么,程序在使用這個未初始化的值時,便會出現錯誤。
針對此問題,可在instance上添加volatile關鍵字,使得instance在讀、寫操作前后都會插入內存屏障,避免重排序。
最終,單例模式實現如下:
public?class?Singleton?{private?static?volatile?Singleton?instance;private?Singleton()?{}public?Singleton?getInstance()?{if?(instance?==?null)?{synchronized?(Singleton.class)?{if?(instance?==?null)?{instance?=?new?Singleton();}}}return?instance;} }至此,一個完善的單例模式實現了。此時,你是否有一個疑問,為什么Nacos中的雙重檢查鎖沒有使用volatile關鍵字呢?
答案很簡單:上面單例模式如果出現指令重排,會導致單例實例被使用。那么,再看Nacos的代碼,由于創建ConcurrentHashSet并不會影響到查詢,而真正影響查詢的是listenerMap.put方法,而ConcurrentHashSet本身是線程安全的。因此,也就不會出現線程安全問題,不用使用volatile關鍵字了。
小結
閱讀源碼最有意思的一個地方就是可以看到很多經典知識的實踐,如果能夠深入思考,拓展一下,會獲得意想不到的收獲。
再回顧一下本文的重點:
閱讀Nacos源碼,發現雙重檢查鎖的使用;
未加鎖單例模式使用,會創建多個對象;
方法上加鎖,導致性能下降;
代碼內局部加鎖,雙重判斷,既滿足線程安全,又滿足性能需求;
單例模式特例:創建對象分多步,會出現指令重排現象,采用volatile進行避免指令重排;
最后,想學習更多類似干貨,關注一下吧,持續輸出。
往期推薦ReentrantLock 中的 4 個坑!
synchronized 中的 4 個優化,你知道幾個?
synchronized 加鎖 this 和 class 的區別!
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的双重检查锁,原来是这样演变来的,你了解吗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java LineNumberReade
- 下一篇: Java ClassLoader get