真快,金三銀四面試季就要過去了,你拿到心儀的offer了嗎?


          因為這次疫情你覺得面試簡單了還是更難了?我覺得既簡單又難,簡單是因為不需要背著包到處跑,不需要打印簡歷,都是電話面、視頻面,非常的便利,難是因為有很多中小公司因此而裁員甚至倒閉。


          我的一個小伙伴也趁著這個機會面了幾家試了試水,其中有面試官問到了一個問題:使用過單例模式嗎?單例模式有哪些實現(xiàn)方式?你用過哪些?你的單例模式能保證百分之百單例嗎?

          朋友就列舉了幾種實現(xiàn)方式并且比較了幾種方式的優(yōu)缺點,但對于最后一個問題他當時就想:單例模式不就是單例的嗎?事后我告訴他真相,他才恍然大悟,連連感謝

          我猜肯定還有不少小伙伴不知道這個,所以今天就科普一下單例模式,如何打破單例模式以及如何保證百分百的單例。其實我很早前就寫過一篇類似的文章,誰叫你不看呢

          ?

          ?

          單例模式的基本概念

          什么是單例

          單例模式是Java設計模式中最簡單也是最常用的模式之一。所謂單例就是在系統(tǒng)中只有一個該類的實例,并且提供一個訪問該實例的全局訪問方法。

          單例的實現(xiàn)步驟

          單例模式的實現(xiàn)分為三個步驟:

          *
          構造方法私有化。即不能在類外實例化,只能在類內(nèi)實例化。

          *
          在本類中創(chuàng)建本類的實例。必須自己創(chuàng)建該唯一實例。

          *
          在本類中提供給外部獲取實例的方式。提供訪問該實例的全局訪問方法。

          單例模式常見應用場景

          *
          Windows任務管理器

          *
          數(shù)據(jù)庫連接池

          *
          Java中的Runtime

          *
          Spring中Bean的默認生命周期??

          單例模式的優(yōu)點

          *
          提供了唯一實例的全局訪問方法,可以優(yōu)化共享資源的訪問

          *
          避免對象的頻繁創(chuàng)建和銷毀,可以提高性能?

          單例的具體實現(xiàn)方式

          餓漢式-靜態(tài)變量

          餓漢式的特點就是立即創(chuàng)建,不管現(xiàn)在需不需要,先創(chuàng)建實例。關鍵在于“餓”,餓了就要立即吃。
          public class Singleton{ //靜態(tài)變量保存實例變量 public static Singleton instance = new
          Singleton();//構造器私有化 private Singleton() { } //提供訪問該實例的全局訪問方法 public static
          Singleton getInstance(){return instance; } }
          ?


          這里將類的構造器私有化,就不能在外部通過new關鍵字創(chuàng)建該類的實例,然后定義了一個該類的私有靜態(tài)變量,接著定義了一個公有getInstance()方法以便外部能夠獲得該類實例。

          優(yōu)點

          getInstance()性能好,線程安全,實現(xiàn)簡單。

          由于使用了static關鍵字,保證了在引用這個變量時,關于這個變量的所以寫入操作都完成,所以保證了JVM層面的線程安全。

          缺點

          不能實現(xiàn)懶加載,造成空間浪費。

          如果一個類比較大,我們在初始化的時就加載了這個類,但是我們長時間沒有使用這個類,這就導致了內(nèi)存空間的浪費。

          ?

          餓漢式-靜態(tài)代碼塊


          這種方式和上面的靜態(tài)常量/變量類似,只不過把new放到了靜態(tài)代碼塊里,從簡潔程度上比不過第一種。但是把new放在static代碼塊有別的好處,那就是可以做一些別的操作,如初始化一些變量,從配置文件讀一些數(shù)據(jù)等。
          /** * 餓漢模式-靜態(tài)代碼塊 */ public class HungryStaticBlockSingleton{ //構造器私有化 private
          HungryStaticBlockSingleton() { }//靜態(tài)變量保存實例變量 public static final
          HungryStaticBlockSingleton INSTANCE;static { INSTANCE = new
          HungryStaticBlockSingleton(); } }
          ?

          如下,在static代碼塊里讀取 info.properties 配置文件動態(tài)配置的屬性,賦值給 info 字段。
          /** * 餓漢模式-靜態(tài)代碼塊 * 這種用于可以在靜態(tài)代碼塊進行一些初始化 */ public class
          HungryStaticBlockSingleton{private String info; private
          HungryStaticBlockSingleton(String info) {this.info = info; } //構造器私有化 private
          HungryStaticBlockSingleton() { }//靜態(tài)變量保存實例變量 public static
          HungryStaticBlockSingleton instance;static { Properties properties = new
          Properties();try { properties.load(HungryStaticBlockSingleton.class
          .getClassLoader().getResourceAsStream("info.properties")); } catch (IOException
          e) { e.printStackTrace(); } instance= new
          HungryStaticBlockSingleton(properties.getProperty("info")); } //getter and
          setter... }
          ?

          Test
          public class HungrySingletonTest{ public static void main(String[] args) {
          HungryStaticBlockSingleton hun= HungryStaticBlockSingleton.INSTANCE;
          System.out.println(hun.getInfo()); } }
          ?

          輸出



          ?

          ?

          ?

          懶漢式

          需要時再創(chuàng)建,關鍵在于“懶”,類似懶加載。
          public class Singleton1 { //定義靜態(tài)實例對象,但不初始化 private static Singleton1 instance =
          null; //構造方法私有化 private Singleton1() { } //提供全局訪問方法 public static Singleton1
          getInstance() {if (instance == null) { instance = new Singleton1(); } return
          instance; } }
          同樣是構造方法私有化,提供給外部獲得實例的方法,getInstance()方法被調用時創(chuàng)建實例。

          優(yōu)點

          getInstance()性能好,延遲初始化

          缺點

          適用于單線程環(huán)境,多線程下可能發(fā)生線程安全問題,導致創(chuàng)建不同實例的情況發(fā)生。
          public class LazyUnsafeSingletionTest{ public static void main(String[] args)
          throws ExecutionException, InterruptedException { ExecutorService es =
          Executors.newFixedThreadPool(2); Callable<Singleton1> c1 = new
          Callable<Singleton1>(){ @Override public Singleton1 call() throws Exception {
          return Singleton1.getInstance(); } }; Callable<Singleton1> c2 = new
          Callable<Singleton1>(){ @Override public Singleton1 call() throws Exception {
          return Singleton1.getInstance(); } }; Future<Singleton1> submit =
          es.submit(c1); Future<Singleton1> submit1 = es.submit(c2); Singleton1
          lazyUnsafeSingleton= submit.get(); Singleton1 lazyUnsafeSingleton1 =
          submit1.get(); es.shutdown(); System.out.println(lazyUnsafeSingleton);
          System.out.println(lazyUnsafeSingleton); System.out.println(lazyUnsafeSingleton1
          ==lazyUnsafeSingleton); } }
          ?

          可以看下面的演示。非線程安全演示:

          輸出?



          ?

          ?

          ?

          大概運行三次就會出現(xiàn)一次,我們可以在Singleton1中增加一個判斷,在?if(instance==null) 之后增加一行線程休眠的代碼以獲得更好的效果。

          ?

          懶漢式 + synchronized

          通過使用synchronized修飾getInstance()方法保證同步訪問該方法,但是訪問性能不高。
          public class Singleton1 { //定義靜態(tài)實例對象,但不初始化 private static Singleton1 instance =
          null; //構造方法私有化 private Singleton1() { } //提供全局訪問方法 synchronized同步訪問getInstance
          public static synchronized Singleton1 getInstance() { if (instance == null) {
          instance= new Singleton1(); } return instance; } }
          ?

          優(yōu)點

          線程安全,延遲初始化

          缺點

          getInstance()性能不好(使用了synchronized修飾訪問需要同步,并發(fā)訪問性能不高)

          ?

          懶漢式 + Double check

          解決懶漢式 + synchronized 訪問性能不高的問題
          public class Singleton1 { //定義靜態(tài)實例對象,但不初始化 private static Singleton1 instance =
          null; //構造方法私有化 private Singleton1() { } //提供全局訪問方法 synchronized同步控制創(chuàng)建實例 public
          static Singleton1 getInstance() { if (instance == null) { synchronized
          (Singleton1.class) { if (instance == null) { instance = new Singleton1(); } } }
          return instance; } }
          ?

          優(yōu)點

          getInstance()訪問性能高,延遲初始化

          缺點

          非線程安全?


          該方式通過縮小同步范圍提高訪問性能,同步代碼塊控制并發(fā)創(chuàng)建實例。并且采用雙重檢驗,當兩個線程同時執(zhí)行第一個判空時,都滿足的情況下,都會進來,然后去爭鎖,假設線程1拿到了鎖,執(zhí)行同步代碼塊的內(nèi)容,創(chuàng)建了實例并返回,釋放鎖,然后線程2獲得鎖,執(zhí)行同步代碼塊內(nèi)的代碼,因為此時線程1已經(jīng)創(chuàng)建了,所以線程2雖然拿到鎖了,如果內(nèi)部不加判空的話,線程2會再new一次,導致兩個線程獲得的不是同一個實例。線程安全的控制其實是內(nèi)部判空在起作用,至于為什么要加外面的判空下面會說。

          ?

          當不加內(nèi)層判空時,會出現(xiàn)不是單例的情況,只不過出現(xiàn)的概率更低了點。



          ?

          ?

          ?

          可不可以只加內(nèi)層判空呢?

          答案是可以。

          那為什么還要加外層判空的呢?

          內(nèi)層判空已經(jīng)可以滿足線程安全了,加外層判空的目的是為了提高效率。


          因為可能存在這樣的情況:如果不加外層判空,線程1拿到鎖后執(zhí)行同步代碼塊,在new之后,還沒有釋放鎖的時候,線程2過來了,它在等待鎖(此時線程1已經(jīng)創(chuàng)建了實例,只不過還沒釋放鎖,線程2就來了),然后線程1釋放鎖后,線程2拿到鎖,進入同步代碼塊中,判空不成立,直接返回實例。

          這種情況線程2是不是不用去等待鎖了?因為線程1已經(jīng)創(chuàng)建了實例,只不過還沒釋放鎖。

          所以在外層又加了一個判空就是為了防止這種情況,線程2過來后先判空,不為空就不用去等待鎖了,這樣提高了效率。

          ?

          懶漢式 + Double check + volatile


          雙重檢查鎖模式是一種非常好的單例實現(xiàn)模式,解決了單例、性能問題,上面的雙重檢測鎖模式看上去完美無缺,其實是存在問題,那就是上面缺點中,線程安全后面打問號的原因。

          在多線程的情況下,雙重檢查鎖模式可能會出現(xiàn)空指針問題,出現(xiàn)問題的原因是JVM在實例化對象的時候會進行優(yōu)化和指令重排序操作。什么是指令重排
          ?上面的instance = new Singleton1();這行代碼并不是一個原子指令,會被分割成多個指令:
          memory = allocate(); //1:分配對象的內(nèi)存空間 ctorInstance(memory); //2:初始化對象 instance =
          memory;//3:設置instance指向剛分配的內(nèi)存地址
          ?

          經(jīng)過指令重排后的代碼順序:
          memory = allocate(); //1:分配對象的內(nèi)存空間 instance = memory; //
          3:設置instance指向剛分配的內(nèi)存地址,此時對象還沒被初始化 ctorInstance(memory); //2:初始化對象
          ?

          實例化對象實際上可以分解成以下4個步驟:

          *
          為對象分配內(nèi)存空間

          *
          初始化默認值(區(qū)別于構造器方法的初始化),

          *
          執(zhí)行構造器方法

          將對象指向剛分配的內(nèi)存空間

          編譯器或處理器為了性能的原因,可能會將第3步和第4步進行重排序:

          *
          為對象分配內(nèi)存空間

          *
          初始化默認值 ?

          *
          將對象指向剛分配的內(nèi)存空間

          *
          執(zhí)行構造器方法?

          線程可能獲得一個初始化未完成的對象......

          ?


          若有線程1進行完重排后的第二步,且未執(zhí)行初始化對象。此時線程2來取instance時,發(fā)現(xiàn)instance不為空,于是便返回該值,但由于沒有初始化完該對象,此時返回的對象是有問題的。這也就是為什么說看似穩(wěn)的一逼的代碼,實則不堪一擊。?上述代碼的改進方法:將instance聲明為volatile類型即可(volatile有內(nèi)存屏障的功能)。
          private static volatile Singleton1 instance = null;
          ?

          內(nèi)部類


          該方式天然線程安全,適用于多線程,利用了內(nèi)部類的特性:加載外部類時不會加載內(nèi)部類,在內(nèi)部類被加載和初始化時,才創(chuàng)建實例。靜態(tài)內(nèi)部類不會自動隨著外部類的加載和初始化而初始化,它是要單獨加載和初始化的。因為我們的單例對象是在內(nèi)部類加載和初始化時才創(chuàng)建的,因此它是線程安全的,且實現(xiàn)了延遲初始化。
          public class LazyInnerSingleton{ private LazyInnerSingleton() { } private
          static class Inner{ private static LazyInnerSingleton instance = new
          LazyInnerSingleton(); }public static LazyInnerSingleton getInstance(){ return
          Inner.instance; } }
          ?

          優(yōu)點

          getInstance()訪問性能高,延遲初始化,線程安全

          ?

          前面實現(xiàn)方式可能存在的問題:

          *
          需要額外的工作來實現(xiàn)序列化,否則每次反序列化一個序列化的對象時都會創(chuàng)建一個新的實例,如果沒有自定義序列化方式則單例有被破壞的風險。

          *
          可以使用反射強行調用私有構造器,單例有被破壞的風險。

          《Effective Java》中推薦使用Enum來創(chuàng)建單例對象

          *
          枚舉類很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創(chuàng)建新的對象。

          枚舉

          這種方式是最簡潔的,不需要考慮構造方法私有化。值得注意的是枚舉類不允許被繼承,因為枚舉類編譯后默認為final
          class,可防止被子類修改。常量類可被繼承修改、增加字段等,容易導致父類的不兼容。枚舉類型是線程安全的,并且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現(xiàn)單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現(xiàn)中
          唯一一種不會被破壞的單例實現(xiàn)模式。
          public enum SingletonEnum { INSTANCE; public void otherMethod(){
          System.out.println("枚舉類里的方法"); } }
          Test,打印實例直接輸出了【INSTANCE】,是因為枚舉幫我們實現(xiàn)了toString,默認打印名稱。
          public class SingletonEnumTest { public static void main(String[] args) {
          SingletonEnum instance= SingletonEnum.INSTANCE; System.out.println(instance);
          instance.otherMethod(); } }
          輸出結果



          優(yōu)點

          getInstance()訪問性能高,線程安全

          缺點

          非延遲初始化

          ?

          破壞單例模式的方法及預防措施

          上面介紹枚舉實現(xiàn)單例模式前已經(jīng)介紹了除枚舉外的其他單例模式實現(xiàn)方式存在的兩個問題,也正是這兩個問題,導致了單例模式若不采取措施,會有被破壞的可能。

          1、除枚舉方式外,其他方法都會通過反射的方式破壞單例。

          反射是通過強行調用私有構造方法生成新的對象,所以如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有實例,,則阻止生成新的實例,解決辦法如下:
          private Singleton(){ if (instance != null){ throw new
          RuntimeException("實例已經(jīng)存在,請通過 getInstance()方法獲取"); } }
          2、如果單例類實現(xiàn)了序列化接口Serializable, 就可以通過反序列化破壞單例。

          所以我們可以不實現(xiàn)序列化接口,如果非得實現(xiàn)序列化接口,可以重寫反序列化方法readResolve(),反序列化時直接返回相關單例對象。
          public Object readResolve() throws ObjectStreamException { return instance; }
          ?

          總結

          單例模式,從加載時機方面來說分為餓漢模式和懶漢模式,從程序安全性方面來說分為線程安全和非線程安全的。最后總結一下單例模式各種實現(xiàn)方式的優(yōu)缺點。


          方式

          優(yōu)點

          缺點


          餓漢式 - 靜態(tài)變量

          線程安全,訪問性能高

          不能延遲初始化


          餓漢式 - 靜態(tài)代碼塊

          線程安全,訪問性能高,支持額外操作

          不能延遲初始化


          懶漢式

          訪問性能高,延遲初始化

          非線程安全


          懶漢式 + synchronized

          線程安全,延遲初始化

          訪性能不高


          懶漢式 + Double check

          線程安全,延遲初始化

          非線程安全


          懶漢式 + Double check + volatile

          線程安全,延遲初始化,訪問性能高

          -


          內(nèi)部類

          線程安全,延遲初始化,訪問性能高

          -


          枚舉

          線程安全,訪問性能高,安全

          不能延遲初始化

          后三種用的較多,根據(jù)自己的實際場景選擇不同的單例模式。

          更多技術干貨歡迎關注公眾號“編程大道”



          ?

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

                亚洲va在线 | 久肏网 | 免费一区视频 | 美女被操的视频在线观看 | 人妻奴契约竹内纱里奈 |