偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

雙重檢查鎖,原來(lái)是這樣演變來(lái)的,你了解嗎

開(kāi)發(fā) 前端
在看Nacos的源代碼時(shí),發(fā)現(xiàn)多處都使用了“雙重檢查鎖”的機(jī)制,算是非常好的實(shí)踐案例。這篇文章就著案例來(lái)分析一下雙重檢查鎖的使用以及優(yōu)勢(shì)所在,目的就是讓你的代碼格調(diào)更加高一個(gè)層次。

[[417932]]

本文轉(zhuǎn)載自微信公眾號(hào)「程序新視界」,作者二師兄。轉(zhuǎn)載本文請(qǐng)聯(lián)系程序新視界公眾號(hào)。

在看Nacos的源代碼時(shí),發(fā)現(xiàn)多處都使用了“雙重檢查鎖”的機(jī)制,算是非常好的實(shí)踐案例。這篇文章就著案例來(lái)分析一下雙重檢查鎖的使用以及優(yōu)勢(shì)所在,目的就是讓你的代碼格調(diào)更加高一個(gè)層次。

同時(shí),基于單例模式,講解一下雙重檢查鎖的演變過(guò)程。

Nacos中的雙重檢查鎖

在Nacos的InstancesChangeNotifier類中,有這樣一個(gè)方法:

  1. private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>(); 
  2.  
  3. private final Object lock = new Object(); 
  4.  
  5. public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) { 
  6.     String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters); 
  7.     ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key); 
  8.     if (eventListeners == null) { 
  9.         synchronized (lock) { 
  10.             eventListeners = listenerMap.get(key); 
  11.             if (eventListeners == null) { 
  12.                 eventListeners = new ConcurrentHashSet<EventListener>(); 
  13.                 listenerMap.put(key, eventListeners); 
  14.             } 
  15.         } 
  16.     } 
  17.     eventListeners.add(listener); 

該方法的主要功能就是對(duì)監(jiān)聽(tīng)器事件進(jìn)行注冊(cè)。其中注冊(cè)的事件都存在成員變量listenerMap當(dāng)中。listenerMap的數(shù)據(jù)結(jié)構(gòu)是key為String,value為ConcurrentHashSet的Map。也就是說(shuō),一個(gè)key對(duì)應(yīng)一個(gè)集合。

針對(duì)這種數(shù)據(jù)結(jié)構(gòu),在多線程的情況下,Nacos處理流程如下:

  • 通過(guò)key獲取value值;
  • 判斷value是否為null;
  • 如果value值不為null,則直接將值添加到Set當(dāng)中;
  • 如果為null,就需要?jiǎng)?chuàng)建一個(gè)ConcurrentHashSet,在多線程時(shí),有可能會(huì)創(chuàng)建多個(gè),因此要使用鎖。
  • 通過(guò)synchronized鎖定一個(gè)Object對(duì)象;
  • 在鎖內(nèi)再獲取一次value值,如果依然是null,則進(jìn)行創(chuàng)建。
  • 進(jìn)行后續(xù)操作。

上述過(guò)程,在鎖定前和鎖定之后,做了兩次判斷,因此稱作”雙重檢查鎖“。使用鎖的目的就是避免創(chuàng)建多個(gè)ConcurrentHashSet。

Nacos中的實(shí)例稍微復(fù)雜一下,下面以單例模式中的雙重檢查鎖的演變過(guò)程。

未加鎖的單例

這里直接演示單例模式的懶漢模式實(shí)現(xiàn):

  1. public class Singleton { 
  2.      
  3.     private static Singleton instance; 
  4.      
  5.     private Singleton() { 
  6.     } 
  7.      
  8.     public Singleton getInstance() { 
  9.         if (instance == null) { 
  10.             instance = new Singleton(); 
  11.         } 
  12.         return instance; 
  13.     }     

這是一個(gè)最簡(jiǎn)單的單例模式,在單線程下運(yùn)轉(zhuǎn)良好。但在多線程下會(huì)出現(xiàn)明顯的問(wèn)題,可能會(huì)創(chuàng)建多個(gè)實(shí)例。

以兩個(gè)線程為例:

可以看到,當(dāng)兩個(gè)線程同時(shí)執(zhí)行時(shí),是有可能會(huì)創(chuàng)建多個(gè)實(shí)例的,這很明顯不符合單例的要求。

加鎖單例

針對(duì)上述代碼的問(wèn)題,很直觀的想到是進(jìn)行加鎖處理,實(shí)現(xiàn)代碼如下:

  1. public class Singleton { 
  2.      
  3.     private static Singleton instance; 
  4.      
  5.     private Singleton() { 
  6.     } 
  7.      
  8.     public synchronized Singleton getInstance() { 
  9.         if (instance == null) { 
  10.             instance = new Singleton(); 
  11.         } 
  12.         return instance; 
  13.     } 

與第一個(gè)示例唯一的區(qū)別是在方法上添加了synchronized關(guān)鍵字。這時(shí),當(dāng)多個(gè)線程進(jìn)入該方法時(shí),需要先獲得鎖才能進(jìn)行執(zhí)行。

通過(guò)在方法上添加synchronized關(guān)鍵字,看似完美的解決了多線程的問(wèn)題,但卻帶了性能問(wèn)題。

我們知道使用鎖會(huì)導(dǎo)致額外的性能開(kāi)銷,對(duì)于上面的單例模式,只有第一次創(chuàng)建時(shí)需要鎖(防止創(chuàng)建多個(gè)實(shí)例),但查詢時(shí)是不需要鎖的。

如果針對(duì)方法進(jìn)行加鎖,每次查詢也要承擔(dān)加鎖的性能損耗。

雙重檢查鎖

針對(duì)上面的問(wèn)題,就有了雙重檢查鎖,示例如下:

  1. public class Singleton { 
  2.      
  3.     private static Singleton instance; 
  4.      
  5.     private Singleton() { 
  6.     } 
  7.      
  8.     public Singleton getInstance() { 
  9.         if (instance == null) { 
  10.             synchronized (Singleton.class) { 
  11.                 if (instance == null) { 
  12.                     instance = new Singleton(); 
  13.                 } 
  14.             } 
  15.         } 
  16.         return instance; 
  17.     } 

第一,將鎖的范圍縮小的方法內(nèi);

第二,鎖之前先判斷一下是不是null,如果不為null,說(shuō)明已經(jīng)實(shí)例化了,直接返回,沒(méi)必要進(jìn)行創(chuàng)建;

第三,如果為null,進(jìn)行加鎖,然后再次判斷是否為null。為什么要再次判斷?因?yàn)橐粋€(gè)線程判斷為null之后,另外一個(gè)線程可能已經(jīng)創(chuàng)建了對(duì)象,所以在鎖定之后,需要再次核實(shí)一下,真的為null,則進(jìn)行對(duì)象創(chuàng)建。

改進(jìn)之后,既保證了線程的安全性,又避免了鎖導(dǎo)致的性能損失。問(wèn)題到此結(jié)束了嗎?并沒(méi)有,繼續(xù)往下看。

JVM的指令重排

在某些JVM當(dāng)中,編譯器為了性能問(wèn)題,會(huì)進(jìn)行指令重排。在上述代碼中new Singleton()并不是原子操作,有可能會(huì)被編譯器進(jìn)行重排操作。

創(chuàng)建對(duì)象可抽象為三步:

  1. memory = allocate();    //1:分配對(duì)象的內(nèi)存空間  
  2. ctorInstance(memory);  //2:初始化對(duì)象  
  3. instance = memory;     //3:設(shè)置instance指向剛分配的內(nèi)存地址 

上面操作中,操作2依賴于操作1,但操作3并不依賴于操作2。因此,JVM是可以進(jìn)行指令重排優(yōu)化的,可能會(huì)出現(xiàn)如下的執(zhí)行順序:

  1. memory = allocate();    //1:分配對(duì)象的內(nèi)存空間  
  2. instance = memory;     //3:instance指向剛分配的內(nèi)存地址,此時(shí)對(duì)象還未初始化 
  3. ctorInstance(memory);  //2:初始化對(duì)象 

指令重排之后,將操作3的賦值操作放在了前面,那就會(huì)出現(xiàn)一個(gè)問(wèn)題:當(dāng)線程A執(zhí)行完步驟賦值操作,但還未執(zhí)行對(duì)象初始化。此時(shí),線程B進(jìn)來(lái)了,在第一層判斷時(shí)發(fā)現(xiàn)Instance已經(jīng)有值了(實(shí)際上還未初始化),直接返回對(duì)應(yīng)的值。那么,程序在使用這個(gè)未初始化的值時(shí),便會(huì)出現(xiàn)錯(cuò)誤。

針對(duì)此問(wèn)題,可在instance上添加volatile關(guān)鍵字,使得instance在讀、寫(xiě)操作前后都會(huì)插入內(nèi)存屏障,避免重排序。

最終,單例模式實(shí)現(xiàn)如下:

  1. public class Singleton { 
  2.      
  3.     private static volatile Singleton instance; 
  4.      
  5.     private Singleton() { 
  6.     } 
  7.      
  8.     public Singleton getInstance() { 
  9.         if (instance == null) { 
  10.             synchronized (Singleton.class) { 
  11.                 if (instance == null) { 
  12.                     instance = new Singleton(); 
  13.                 } 
  14.             } 
  15.         } 
  16.         return instance; 
  17.     } 

至此,一個(gè)完善的單例模式實(shí)現(xiàn)了。此時(shí),你是否有一個(gè)疑問(wèn),為什么Nacos中的雙重檢查鎖沒(méi)有使用volatile關(guān)鍵字呢?

答案很簡(jiǎn)單:上面單例模式如果出現(xiàn)指令重排,會(huì)導(dǎo)致單例實(shí)例被使用。那么,再看Nacos的代碼,由于創(chuàng)建ConcurrentHashSet并不會(huì)影響到查詢,而真正影響查詢的是listenerMap.put方法,而ConcurrentHashSet本身是線程安全的。因此,也就不會(huì)出現(xiàn)線程安全問(wèn)題,不用使用volatile關(guān)鍵字了。

小結(jié)

閱讀源碼最有意思的一個(gè)地方就是可以看到很多經(jīng)典知識(shí)的實(shí)踐,如果能夠深入思考,拓展一下,會(huì)獲得意想不到的收獲。

再回顧一下本文的重點(diǎn):

  • 閱讀Nacos源碼,發(fā)現(xiàn)雙重檢查鎖的使用;
  • 未加鎖單例模式使用,會(huì)創(chuàng)建多個(gè)對(duì)象;
  • 方法上加鎖,導(dǎo)致性能下降;
  • 代碼內(nèi)局部加鎖,雙重判斷,既滿足線程安全,又滿足性能需求;
  • 單例模式特例:創(chuàng)建對(duì)象分多步,會(huì)出現(xiàn)指令重排現(xiàn)象,采用volatile進(jìn)行避免指令重排。

 

責(zé)任編輯:武曉燕 來(lái)源: 程序新視界
相關(guān)推薦

2022-12-14 07:32:40

InnoDBMySQL引擎

2014-07-21 10:32:52

蘋(píng)果公司實(shí)習(xí)

2018-04-02 15:13:21

網(wǎng)絡(luò)

2023-02-15 08:17:38

2024-04-30 08:22:51

Figma圖形編輯變換矩陣

2025-02-17 09:22:16

MySQLSQL語(yǔ)句

2022-05-05 08:55:12

工業(yè)物聯(lián)網(wǎng)IIoT

2024-02-06 09:30:25

Figma矩形矩形物理屬性

2023-05-22 15:58:11

2020-05-26 08:52:36

Java JVM多態(tài)

2022-05-09 08:37:43

IO模型Java

2020-11-24 06:20:02

Linux日志文件系統(tǒng)

2016-10-12 08:54:24

2009-03-11 14:42:57

面試求職案例

2015-03-25 09:55:34

程序員程序員修補(bǔ)BUG真正原因

2018-10-26 10:41:19

2020-03-23 08:30:12

程序員男友感受

2017-05-09 15:39:33

ensorFlow機(jī)器人機(jī)器學(xué)習(xí)

2022-07-13 10:37:59

服務(wù)器故障優(yōu)化

2017-06-06 15:13:07

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)