<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          單例模式,關(guān)鍵字級別詳解

          共 4934字,需瀏覽 10分鐘

           ·

          2021-08-29 10:03

          0.前言

          如果你去問一個寫過幾年代碼的程序員用過哪些設(shè)計模式,我打賭,90%以上的回答里面會帶【單例模式】。甚至有的面試官會直接問:說一下你用過哪些設(shè)計模式,單例就不用說了。你看,連面試官都聽煩了,火爆程度可見一斑。

          不過,看似簡單的單例模式,里面蘊含了很多Java基礎(chǔ),日常開發(fā)過程中課代表見過很多不規(guī)范的,甚至是有問題的單例實現(xiàn)。所以整理此文,總結(jié)一下單例模式的最佳實踐。

          1、懶加載(懶漢)

          所謂懶加載,就是直到第一次被調(diào)用時才加載。其實現(xiàn)需要考慮并發(fā)問題和指令重排,代碼如下:

          public class Singleton {

              private volatile static Singleton instance; //①

              private Singleton() //②
              }

              public static Singleton getInstance() {
                  if (instance == null) {//③
                      synchronized (Singleton.class{
                          if (instance == null) {//④
                              instance = new Singleton();//⑤
                          }
                      }
                  }
                  return instance;
              }
          }

          這段代碼精簡至極,沒有一個字符是多余的,下面逐行解讀一下:

          首先,注意到①處的volatile關(guān)鍵字,它具備兩項特性:

          一是保證此變量對于所有線程的可見性。即當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。

          二是禁止指令重排序優(yōu)化。

          這里解釋一下指令重排序優(yōu)化:

          代碼 ⑤ 處的instance = new Singleton();并不是原子的,大體可分為如下 3 步:

          1. 分配內(nèi)存
          2. 調(diào)用構(gòu)造函數(shù)初始化成實例
          3. instance指向分配的內(nèi)存空間

          JVM 允許在保證結(jié)果正確的前提下進行指令重排序優(yōu)化。即如上 3 步可能的順序為1->2->3 或 1->3->2 。如果順序是 1->3->2 ,當 3 執(zhí)行完,2 還未執(zhí)行時,另一個線程執(zhí)行到代碼 ③ 處,發(fā)現(xiàn)instance不為null,直接返回還未初始化好的instance并使用,就會報錯。

          所以使用volatile,就是為了保證線程間的可見性和防止指令重排。

          其次,代碼②處將構(gòu)造函數(shù)聲明為private目的在于阻止使用new Singleton()這樣的代碼生成新實例。

          最后,當客戶端調(diào)用Singleton.getInstance()時,先檢查是否已經(jīng)實例化(代碼③),未實例化時同步代碼塊,然后再次檢查是否已實例化(代碼④),然后才執(zhí)行代碼⑤。兩次檢查的意義在于,防止synchronized同步過程中其他線程進行了實例化。

          這就是著名的雙重檢查鎖(Double check lock)實現(xiàn)單例,也即懶加載。

          TIPS:

          網(wǎng)上也有直接對getInstance()方法加鎖的版本,這樣大范圍的方法級別加鎖會導(dǎo)致并發(fā)變低,實際上第一次調(diào)用生成實例之后,后續(xù)獲取實例根本不需要并發(fā)控制了。而本例的雙重檢查鎖版本可以避免此并發(fā)問題。

          2、預(yù)加載(餓漢)

          與懶加載相對應(yīng),預(yù)加載是在類加載時就已經(jīng)初始化好了,所以是天然線程安全的,代碼如下:

          public class Singleton {

              private static final Singleton instance = new Singleton();// ①
              
              private Singleton(){}
              
              public static Singleton getInstance(){
                  return instance;
              }
          }

          注意到 ① 處的類變量使用了final

          這里用final更多的意義在于提供語法約束。畢竟你是單例,就只有這一個實例,不可能再指向另一個。instance有了final的約束,后面再有人不小心編寫了修改其指向的代碼就會報語法錯誤。

          這就好比@Override注解,你能保證寫對方法名和參數(shù),那不寫注解也沒問題,但是有了注解的約束,編譯器就會幫你檢查,還能防止別人亂改。

          3、靜態(tài)內(nèi)部類

          此方法和預(yù)加載原理相同,都是利用JVM類加載的特性實現(xiàn)天然的線程安全,不同之處在于,靜態(tài)內(nèi)部類做到了延遲加載。

          public class Singleton {
              
              private static class SingletonHolder {
                  private static Singleton instance = new Singleton();
              }
              
              private Singleton(){}

              public static Singleton getInstance() {
                  return SingletonHolder.instance;
              }
          }

          SingletonHolder 是靜態(tài)內(nèi)部類,當外部類Singleton被加載的時候并不會創(chuàng)建任何實例,只有當Singleton.getInstance()被調(diào)用的時候,才會創(chuàng)建Singleton實例,這一切由 JVM 天然完成,所以既保證了線程安全,又實現(xiàn)了延遲加載。

          4、枚舉

          沒錯,枚舉可以實現(xiàn)單例,而且這種方式是《Effective Java中文版》第二版 中的推薦實現(xiàn)方式。代碼極其簡單:

          public enum Singleton {
              /**
               * 單例實例
               */

              INSTANCE;

              public void doSomeThing(){
                  System.out.println("done");
              }
          }

          使用時直接Singleton.INSTANCE.doSomeThing();即可。

          這里主要利用了枚舉的如下兩個特性:

          • 枚舉的構(gòu)造器總是私有的,所以不必像前幾種方式一樣顯式定義私有構(gòu)造方法
          • 枚舉類中的每個值,都是實例(只有INSTANCE這一個實例)

          除此之外,枚舉還附帶了一些額外好處:無償?shù)靥峁┝诵蛄谢瘷C制,還可以防止通過多次反序列化生成多個實例。

          鑒于此,單例的最佳實踐就是用枚舉來實現(xiàn)。

          5、總結(jié)

          事實上,單例的寫法并不止于本文所提的這 4 種,你可能還會看到很多其他變種,它們或多或少都存在一些缺陷,比如,懶加載方式將synchronized作用于整個方法上也能實現(xiàn),但頻繁加鎖,釋放鎖會產(chǎn)生性能瓶頸,而完全去掉鎖又會帶來并發(fā)問題。

          所以,只要吃透了文中列出的這 4 種單例方式,就能做到舉一反三,見到別人寫的單例也能一眼看出對錯。

          文中所列的 4 種單例模式,除了枚舉之外,全都用到了static關(guān)鍵字,《Java 虛擬機規(guī)范》 規(guī)定,有幾種情況必須立即對類進行“初始化”,其中涉及static的場景如下:

          讀取或設(shè)置一個類型的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候。

          調(diào)用一個類型的靜態(tài)方法的時候。

          懶加載,預(yù)加載和靜態(tài)內(nèi)部類正是利用了這兩點特性。

          static關(guān)鍵字遺忘的同學(xué)可以參看我的另一篇文章:《一題搞定static關(guān)鍵字》

          最后,再次強調(diào)一下,如果大家開發(fā)中需要手寫單例,建議聽從 Joshua Bloch在《Effective Java中文版》第二版 中的建議:

          — 【 THE END 】—
          本公眾號全部博文已整理成一個目錄,請在公眾號里回復(fù)「m」獲取!

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點“在看”,關(guān)注公眾號并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 19
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品自拍小视频 | 亚洲男女激情91免费网站 | 成人夜间视频 | 无码爱爱网站 | 国产9在线观看 |