<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)鍵字級(jí)別詳解

          共 5152字,需瀏覽 11分鐘

           ·

          2021-09-10 15:17


          0.前言

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

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

          1、懶加載(懶漢)

          所謂懶加載,就是直到第一次被調(diào)用時(shí)才加載。其實(shí)現(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;
              }
          }

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

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

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

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

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

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

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

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

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

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

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

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

          TIPS:

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

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

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

          public class Singleton {

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

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

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

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

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

          此方法和預(yù)加載原理相同,都是利用JVM類加載的特性實(shí)現(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)部類,當(dāng)外部類Singleton被加載的時(shí)候并不會(huì)創(chuàng)建任何實(shí)例,只有當(dāng)Singleton.getInstance()被調(diào)用的時(shí)候,才會(huì)創(chuàng)建Singleton實(shí)例,這一切由 JVM 天然完成,所以既保證了線程安全,又實(shí)現(xiàn)了延遲加載。

          4、枚舉

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

          public enum Singleton {
              /**
               * 單例實(shí)例
               */

              INSTANCE;

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

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

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

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

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

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

          5、總結(jié)

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

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

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

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

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

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

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

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

          單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn) Singleton 的最佳方法

          參考資料:

          1、《Effective Java中文版》 Joshua Bloch 第二版 P15

          2、《深入理解 Java 虛擬機(jī)》 周志明 第3版,P444-P448,P264

          3、深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式 (https://coolshell.cn/articles/265.html)

          - END -


          推薦閱讀

          1、Spring Boot+Vue項(xiàng)目實(shí)戰(zhàn)

          2、B站:4小時(shí)上手MyBatis Plus

          3、一文搞懂前后端分離

          4、快速上手Spring Boot+Vue前后端分離


          楠哥簡介

          資深 Java 工程師,微信號(hào) southwindss

          《Java零基礎(chǔ)實(shí)戰(zhàn)》一書作者

          騰訊課程官方 Java 面試官今日頭條認(rèn)證大V

          GitChat認(rèn)證作者,B站認(rèn)證UP主(楠哥教你學(xué)Java)

          致力于幫助萬千 Java 學(xué)習(xí)者持續(xù)成長。




          有收獲,就點(diǎn)個(gè)在看 
          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  性少妇69 | 国语对白久久 | 91黄色视频在线观看 | 五月丁香婷婷综合在线 | 不要网站的黄色电影 |