面試官:請(qǐng)你寫個(gè)單例模式

          你:(太簡(jiǎn)單了吧,我給他來(lái)個(gè)“餓漢式”,再來(lái)個(gè)“懶漢式”)



          (2分鐘后,你的代碼新鮮出爐了)

          餓漢式單例模式代碼
          public class Singleton { private static Singleton instance = new Singleton();
          private Singleton() {} public static Singleton getInstance() { return instance;
          } }
          懶漢式單例模式代碼
          public class LazySingleton { private static LazySingleton instance; private
          LazySingleton() { } public static LazySingleton getInstance() { if (instance ==
          null) { // 1 instance = new LazySingleton(); // 2 } return instance; } }
          (很棒~但是他們真的時(shí)單例嗎)

          代碼分析

          第一段代碼

          instance 是一個(gè)類變量,類變量再類初始化時(shí)創(chuàng)建,類初始化時(shí)相當(dāng)于會(huì)加個(gè)鎖,保證原子性。因此他確實(shí)能保證單例,除非時(shí)多次加載這個(gè)類。

          第二段代碼

          單線程環(huán)境下沒有問題,確實(shí)是單例。

          多線程下則需要考慮下了.

          假設(shè)線程A走到了2,同時(shí)線程B走到了1. 線程A走完了,實(shí)例化了LazySingleton,由于B在A還沒有給instance賦值時(shí)走到了1,所以判斷為
          instance==null, 所以他也會(huì)創(chuàng)建一個(gè)LazySingleton實(shí)例。

          因此此段代碼存在線程安全問題,也就是不能保證LazySingleton是單例的。

          解決方案

          方案一:直接給獲取實(shí)例的方法加鎖

          我們可以通過將getInstance變?yōu)橥椒椒▉?lái)保證同一時(shí)刻只能有一個(gè)線程進(jìn)入到方法。

          如下:
          public class LazySingleton { private static LazySingleton instance; private
          LazySingleton() { } public static synchronized LazySingleton getInstance() { if
          (instance == null) { instance = new LazySingleton(); } return instance; } }
          這種方式簡(jiǎn)單粗暴,但是當(dāng)高并發(fā)去獲取單例時(shí),只有一個(gè)線程能競(jìng)爭(zhēng)到鎖,其他的線程都將阻塞,效率低下。

          方案二:雙重鎖定
          public class DoubleCheckLocking { private static DoubleCheckLocking instance;
          public DoubleCheckLocking() { } public DoubleCheckLocking getInstance() { if
          (instance == null) { // 1 synchronized (DoubleCheckLocking.class) { if
          (instance == null) { instance = new DoubleCheckLocking(); // 問題根源 } } } return
          instance; } }
          這段代碼很巧妙,前一種方法直接將getInstance方法變?yōu)橥椒椒〞?huì)帶來(lái)效率低下問題,那么我們就只在創(chuàng)建對(duì)象的時(shí)候加鎖,這樣既能保證效率,也能保證單例。

          然而,這種方式也有問題,方騰飛老師在《Java并發(fā)編程藝術(shù)》中無(wú)情地嘲諷了這種做法,原文如下:

          因此,人們想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)

          問題的根源就在于new DoubleCheckLocking()這句話,創(chuàng)建一個(gè)對(duì)象大體分為三步, 偽碼表示如下:
          memory=allocate() 1分配對(duì)象內(nèi)存 ctorInstance(memory) 2初始化對(duì)象 instance=memory
          3引用指向創(chuàng)建的對(duì)象
          其中2和3是可能重排序的,因?yàn)樗麄儾淮嬖跀?shù)據(jù)依賴性。也就是3可能在2之前執(zhí)行。

          假設(shè)A線程獲取單例按1、3、2走到了3,那么此時(shí)instance不為null, 此時(shí)B線程走到1處,直接將instance返回了,所以調(diào)用方拿到了一個(gè)
          未被初始化的對(duì)象。

          所以,這個(gè)方法嚴(yán)格來(lái)講是不可取的。

          方案二:改良雙重鎖定

          方案很簡(jiǎn)單,直接在instance變量前加volatile關(guān)鍵字,如下
          private static volatile DoubleCheckLocking instance;
          加上volatile可以阻止上述2、3兩條指令的重排序。

          方案三:基于類初始化
          public class MySingleInstance { private MySingleInstance() { } private static
          class InstanceHolder { public static MySingleInstance instance = new
          MySingleInstance(); } public static MySingleInstance getInstance() { return
          InstanceHolder.instance; } }
          JVM在執(zhí)行類的初始化期間會(huì)去獲取一個(gè)鎖, 這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化。

          一些知識(shí)點(diǎn)

          這里簡(jiǎn)單總結(jié)下以上解決方案中涉及的一些知識(shí)點(diǎn), 只是知識(shí)點(diǎn)的簡(jiǎn)單羅列,后面會(huì)繼續(xù)寫一些文章來(lái)介紹。

          線程

          線程是輕量級(jí)進(jìn)程,是系統(tǒng)調(diào)度的基本單元,線程之間可以共享內(nèi)存變量,每個(gè)線程都有自己獨(dú)立的計(jì)數(shù)器、堆棧和局部變量。

          syncronized

          方案一中我們通過syncronized將獲取實(shí)例的方法同步化了。

          三種形式

          * 普通同步方法,鎖為當(dāng)前實(shí)例對(duì)象
          * 靜態(tài)同步方法,鎖為當(dāng)前類的Class對(duì)象
          * 同步代碼塊,鎖為()里邊的那個(gè)對(duì)象
          基本原理

          在對(duì)象頭存儲(chǔ)鎖信息,基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)同步方法和代碼塊同步。

          volatile

          方案三中,我們通過volatile解決了重排序和內(nèi)存可見性問題。

          volatile的特點(diǎn):

          * 輕量級(jí)的synchronized,不會(huì)引起線程上下文切換
          * 保證共享變量可見性,即一個(gè)線程修改共享變量時(shí),其他線程能讀到修改后的值
          * 加了volatile后,寫操作會(huì)立即從本地內(nèi)存刷新到主內(nèi)存,讀操作會(huì)直接標(biāo)記本地內(nèi)存無(wú)效,從主內(nèi)存中讀取
          這里的本地內(nèi)存只是一個(gè)抽象概念,并非真實(shí)存在

          重排序

          方案二中,我們分析是重排序?qū)е逻@個(gè)方案存在問題。

          重排序是編譯器或處理器為了優(yōu)化程序性能對(duì)指令序列進(jìn)行重新排列的過程。

          分類:

          * 編譯器的指令重排序
          * 處理器的指令重排序
          處理器的指令重排序規(guī)則較為寬松,java編譯器為了防止處理器對(duì)某些指令重排序會(huì)使用內(nèi)存屏障。

          例如上面的volatile, 編譯器生成字節(jié)碼時(shí)會(huì)通過加入內(nèi)存屏障來(lái)阻止cpu對(duì)volatile變量讀寫操作的重排序。

          內(nèi)部類

          在方案三中,我們使用到了內(nèi)部類。內(nèi)部類就是類里邊的類。

          外部類無(wú)法訪問內(nèi)部類成員,只能通過內(nèi)部類的實(shí)例訪問。

          內(nèi)部類可以直接訪問外部類的信息,靜態(tài)內(nèi)部類不能訪問實(shí)例成員。

          按照其所處的不同位置可以分為:

          * 成員內(nèi)部類
          * 靜態(tài)內(nèi)部類
          * 方法內(nèi)部類
          * 匿名內(nèi)部類
          總結(jié)

          本文介紹常見寫單例的方式存在的問題及解決方案,并將解決方案中涉及的重要知識(shí)點(diǎn)做了簡(jiǎn)單羅列。


          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                国产黄视频在线 | 蘑菇视频红色logo | 挺进女同学的屁股眼里小说 | 操免费网站| 国产精品偷窥熟女精品视频 |