<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>

          史上最全單例模式的寫法以及破壞單例方式

          共 8763字,需瀏覽 18分鐘

           ·

          2022-01-10 22:07

          天跟大家講一個老生常談的話題,單例模式是最常用到的設(shè)計模式之一,熟悉設(shè)計模式的朋友對單例模式都不會陌生。網(wǎng)上的文章也很多,但是參差不齊,良莠不齊,要么說的不到點子上,要么寫的不完整,我試圖寫一篇史上最全單例模式,讓你看一篇文章就夠了。

          單例模式定義及應(yīng)用場景

          單例模式是指確保一個類在任何情況下都絕對只有一個實例,并提供一個全局訪問點。單例模式是創(chuàng)建型模式。許多時候整個系統(tǒng)只需要擁有一個全局對象,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為。

          比如在某個服務(wù)器程序中,該服務(wù)器的配置信息存放在一個文件中,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取,然后服務(wù)進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復(fù)雜環(huán)境下的配置管理。

          我們寫單例的思路是,隱藏其所有構(gòu)造方法,提供一個全局訪問點。

          1、餓漢式

          這個很簡單,小伙們都寫過,這個在類加載的時候就立即初始化,因為他很餓嘛,一開始就給你創(chuàng)建一個對象,這個是絕對線程安全的,在線程還沒出現(xiàn)以前就實例化了,不可能存在訪問安全問題。他的缺點是如果不用,用不著,我都占著空間,造成內(nèi)存浪費。

          public class HungrySingleton {

          private static final HungrySingleton hungrySingleton = new HungrySingleton();

          private HungrySingleton() {
          }

          public static HungrySingleton getInstance() {
          return hungrySingleton;
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          還有一種是餓漢式的變種,靜態(tài)代碼塊寫法,原理也是一樣,只要是靜態(tài)的,在類加載的時候就已經(jīng)成功初始化了,這個和上面的比起來沒什么區(qū)別,無非就是裝個b,看起來比上面那種吊,因為見過的人不多嘛。

          public class HungryStaticSingleton {

          private static final HungryStaticSingleton hungrySingleton;

          static {
          hungrySingleton = new HungryStaticSingleton();
          }

          private HungryStaticSingleton() {
          }

          public static HungryStaticSingleton getInstance() {
          return hungrySingleton;
          }

          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          2、懶漢式

          簡單懶漢

          為了解決餓漢式占著茅坑不拉屎的問題,就產(chǎn)生了下面這種簡單懶漢式的寫法,一開始我先申明個對象,但是先不創(chuàng)建他,當用到的時候判斷一下是否為空,如果為空我就創(chuàng)建一個對象返回,如果不為空則直接返回。

          為什么叫懶漢式,就是因為他很懶啊,要等用到的時候才去創(chuàng)建,看上去很ok,但是在多線程的情況下會產(chǎn)生線程安全問題。

          public class LazySimpleSingleton {

          private static LazySimpleSingleton instance;

          private LazySimpleSingleton() {
          }

          public static LazySimpleSingleton getInstance() {
          if (instance == null) {
          instance = new LazySimpleSingleton();
          }
          return instance;
          }

          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          如果有兩個線程同時執(zhí)行到 if (instance==null) 這行代碼,這是判斷都會通過,然后各自會執(zhí)行instance = new Singleton(),并各自返回一個instance,這時候就產(chǎn)生了多個實例,就沒有保證單例,如下圖所示。

          怎么解決這個問題呢,很簡單,加鎖啊,加一下synchronized即可,這樣就能保住線程安全問題了。

          3、雙重校驗鎖(DCL)

          上面這樣寫法帶來一個缺點,就是性能低,只有在第一次進行初始化的時候才需要進行并發(fā)控制,而后面進來的請求不需要在控制了,現(xiàn)在synchronized加在方法上,我管你生成沒成生成,只要來了就得給我排隊,所以這種性能是極其低下的,那怎么辦呢?

          我們知道,其實synchronized除了加在方法上,還可以加在代碼塊上,只要對生成對象的那一部分代碼加鎖就可以了,由此產(chǎn)生一種新的寫法,叫做雙重檢驗鎖,我們看下面代碼。

          我們看19行將synchronized包在了代碼塊上,當 singleton == null 的時候,我們只對創(chuàng)建對象這一塊邏輯進行了加鎖控制,如果 singleton != null 的話,就直接返回,大大提升了效率。

          在21行的時候又加了一個singleton == null,這又是為什么呢,原因是如果兩個線程都到了18行,發(fā)現(xiàn)是空的,然后都進入到代碼塊,這里雖然加了synchronized,但作用只是進行one by one串行化,第一個線程往下走創(chuàng)建了對象,第二個線程等待第一個線程執(zhí)行完畢后,我也往下走,于是乎又創(chuàng)建了一個對象,那還是沒控制住單例,所以在21行當?shù)诙€線程往下走的時候在判斷一次,是不是被別的線程已經(jīng)創(chuàng)建過了,這個就是雙重校驗鎖,進行了兩次非空判斷。

          我們看到在11行的時候加了 volatile 關(guān)鍵字,這是用來防止指令重排的,當我們創(chuàng)建對象的時候會經(jīng)過下面幾個步驟,但是這幾個步驟不是原子的,計算機比較聰明,有時候為了提高效率他不是按順序1234執(zhí)行的,可能是3214執(zhí)行。

          這時候如果第一個線程執(zhí)行了instance = new LazyDoubleCheckSingleton(),由于指令重排先進行了第三步,先分配了一個內(nèi)存地址,第二個線程進來的時候發(fā)現(xiàn)對象已經(jīng)是非null,直接返回,但這時候?qū)ο筮€沒初始化好啊,第二個線程拿到的是一個沒有初始化好的對象!這個就是要加volatile的原因。

          • 分配內(nèi)存給這個對象

          • 初始化對象

          • 設(shè)置instance指向剛分配的內(nèi)存地址

          • 初次訪問對象

          最后說下雙重校驗鎖,雖然提高了性能,但是在我看來不夠優(yōu)雅,折騰來折騰去,一會防這一會防那,尤其是對新手不友好,新手會不明白為什么要這么寫。

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

          上面已經(jīng)將鎖的粒度縮小到創(chuàng)建對象的時候了,但不管加在方法上還是加在代碼塊上,終究還是用到了鎖,只要用到鎖就會產(chǎn)生性能問題,那有沒有不用鎖的方式呢?

          答案是有的,那就是靜態(tài)內(nèi)部類的方式,他其實是利用了java代碼的一種特性,靜態(tài)內(nèi)部類在主類加載的時候是不會被加載的,只有當調(diào)用getInstance()方法的時候才會被加載進來進行初始化,代碼如下

          /**
          * @author jack xu
          * 兼顧餓漢式的內(nèi)存浪費,也兼顧synchronized性能問題
          */
          public class LazyInnerClassSingleton {

          private LazyInnerClassSingleton() {
          }

          public static final LazyInnerClassSingleton getInstance() {
          return LazyHolder.INSTANCE;
          }

          private static class LazyHolder {
          private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
          }

          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          • 17

          • 18

          好,講到這里我已經(jīng)介紹了五種單例的寫法,經(jīng)過層層的演進推理,到第五種的時候已經(jīng)是很完美的寫法了,既兼顧餓漢式的內(nèi)存浪費,也兼顧synchronized性能問題。

          那他真的一定完美嗎,其實不然,他還有一個安全的問題,接下來我們講下單例的破壞,有兩種方式反射和序列化。

          單例的破壞

          1、反射

          我們知道在上面單例的寫法中,在構(gòu)造方法上加上private關(guān)鍵字修飾,就是為了不讓外部通過new的方式來創(chuàng)建對象,但還有一種暴力的方法,我就是不走尋常路,你不讓我new是吧,我反射給你創(chuàng)建出來,代碼如下

          /**
          * @author jack xu
          */
          public class ReflectDestroyTest {

          public static void main(String[] args) {
          try {
          Class clazz = LazyInnerClassSingleton.class;
          Constructor c = clazz.getDeclaredConstructor(null);
          c.setAccessible(true);
          Object o1 = c.newInstance();
          Object o2 = c.newInstance();
          System.out.println(o1 == o2);
          } catch (Exception e) {
          e.printStackTrace();
          }
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          • 17

          • 18

          上面c.setAccessible(true)就是強吻,你private了,我現(xiàn)在把你的權(quán)限設(shè)為true,我照樣能夠訪問,通過c.newInstance()調(diào)用了兩次構(gòu)造方法,相當于new了兩次,我們知道 == 比的是地址,最后結(jié)果是false,確實是創(chuàng)建了兩個對象,反射破壞單例成功。

          那么如何防止反射呢,很簡單,就是在構(gòu)造方法中加一個判斷

          public class LazyInnerClassSingleton {

          private LazyInnerClassSingleton() {
          if (LazyHolder.INSTANCE != null) {
          throw new RuntimeException("不要試圖用反射破壞單例模式");
          }
          }

          public static final LazyInnerClassSingleton getInstance() {
          return LazyHolder.INSTANCE;
          }

          private static class LazyHolder {
          private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          在看結(jié)果,防止反射成功,當調(diào)用構(gòu)造方法時,發(fā)現(xiàn)單例實例對象已經(jīng)不為空了,拋出異常,不讓你在繼續(xù)創(chuàng)建了。

          2、序列化

          接下來介紹單例的另一種破壞方式,先在靜態(tài)內(nèi)部類上實現(xiàn)Serializable接口,然后寫個測試方法測試下,先創(chuàng)建一個對象,然后把這個對象先序列化,然后在反序列化出來,然后對比一下

              public static void main(String[] args) {

          LazyInnerClassSingleton s1 = null;
          LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

          FileOutputStream fos = null;
          try {

          fos = new FileOutputStream("SeriableSingleton.obj");
          ObjectOutputStream oos = new ObjectOutputStream(fos);
          oos.writeObject(s2);
          oos.flush();
          oos.close();

          FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
          ObjectInputStream ois = new ObjectInputStream(fis);
          s1 = (LazyInnerClassSingleton) ois.readObject();
          ois.close();

          System.out.println(s1);
          System.out.println(s2);
          System.out.println(s1 == s2);

          } catch (Exception e) {
          e.printStackTrace();
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          • 17

          • 18

          • 19

          • 20

          • 21

          • 22

          • 23

          • 24

          • 25

          • 26

          • 27

          我們來看結(jié)果發(fā)現(xiàn)是false,在進行反序列化時,在ObjectInputStream的readObject生成對象的過程中,其實會通過反射的方式調(diào)用無參構(gòu)造方法新建一個對象,所以反序列化后的對象和手動創(chuàng)建的對象是不一致的。

          那么怎么避免呢,依然很簡單,在靜態(tài)內(nèi)部類里加一個readResolve方法即可

              private Object readResolve() {
          return LazyHolder.INSTANCE;
          }
          • 1

          • 2

          • 3

          在看結(jié)果就變成true了,為什么加了一個方法就可以避免被序列化破壞呢,這里不在展開,感興趣的小伙伴可以看下ObjectInputStream的readObject()方法,一步步往下走,會發(fā)現(xiàn)最終會調(diào)用readResolve()方法。

          至此,史上最牛b單例產(chǎn)生,已經(jīng)無懈可擊、無可挑剔了。

          3、枚舉

          那么這里我為什么還要在介紹枚舉呢,在《Effective Java》中,枚舉是被推薦的一種方式,因為他足夠簡單,線程安全,也不會被反射和序列化破壞。

          大家看下才寥寥幾句話,不像上面雖然已經(jīng)實現(xiàn)了最牛b的寫法,但是其中的過程很讓人煩惱啊,要考慮性能、內(nèi)存、線程安全、破壞啊,一會這里加代碼一會那里加代碼,才能達到最終的效果。

          而使用枚舉,感興趣的小伙伴可以反編譯看下,枚舉的底層其實還是一個class類,而我們考慮的這些問題JDK源碼其實幫我們都已經(jīng)實現(xiàn)好了,所以在 java 層面我們只需要用三句話就能搞定!

          public enum Singleton {  
          INSTANCE;
          public void whateverMethod() {
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          至此,我通過層層演進,由淺入深的給大家介紹了單例的這么多寫法,從不完美到完美,這么多也是網(wǎng)上很常見的寫法,下面我在送大家兩個彩蛋,擴展一下其他寫單例的方式方法。

          彩蛋

          1、容器式單例

          容器式單例是我們 spring 中管理單例的模式,我們平時在項目中會創(chuàng)建很多的Bean,當項目啟動的時候spring會給我們管理,幫我們加載到容器中,他的思路方式方法如下。

          public class ContainerSingleton {
          private ContainerSingleton() {
          }

          private static Map ioc = new ConcurrentHashMap();

          public static Object getInstance(String className) {
          Object instance = null;
          if (!ioc.containsKey(className)) {
          try {
          instance = Class.forName(className).newInstance();
          ioc.put(className, instance);
          } catch (Exception e) {
          e.printStackTrace();
          }
          return instance;
          } else {
          return ioc.get(className);
          }
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          • 17

          • 18

          • 19

          • 20

          • 21

          這個可以說是一個簡易版的 spring 管理容器,大家看下這里用一個map來保存對象,當對象存在的時候直接從map里取出來返回出去,如果不存在先用反射創(chuàng)建一個對象出來,先保存到map中然后在返回出去。我們來測試一下,先創(chuàng)建一個Pojo對象,然后兩次從容器中去取出來,比較一下,發(fā)現(xiàn)結(jié)果是true,證明兩次取出的對象是同一個對象。

          但是這里有一個問題,這樣的寫法是線程不安全的,那么如何做到線程安全呢,這個留給小伙伴自行獨立思考完成。

          2、CAS單例

          從一道面試題開始:不使用synchronized和lock,如何實現(xiàn)一個線程安全的單例?我們知道,上面講過的所有方式中,只要是線程安全的,其實都直接或者間接用到了synchronized,間接用到是什么意思呢,就比如餓漢式、靜態(tài)內(nèi)部類、枚舉,其實現(xiàn)原理都是利用借助了類加載的時候初始化單例,即借助了ClassLoader的線程安全機制。

          所謂ClassLoader的線程安全機制,就是ClassLoader的loadClass方法在加載類的時候使用了synchronized關(guān)鍵字。也正是因為這樣, 除非被重寫,這個方法默認在整個裝載過程中都是同步的,也就是保證了線程安全。

          那么答案是什么呢,就是利用CAS樂觀鎖,他雖然名字中有個鎖字,但其實是無鎖化技術(shù),當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試,代碼如下:

          /**
          * @author jack xu
          */
          public class CASSingleton {
          private static final AtomicReference INSTANCE = new AtomicReference();

          private CASSingleton() {
          }

          public static CASSingleton getInstance() {
          for (; ; ) {
          CASSingleton singleton = INSTANCE.get();
          if (null != singleton) {
          return singleton;
          }

          singleton = new CASSingleton();
          if (INSTANCE.compareAndSet(null, singleton)) {
          return singleton;
          }
          }
          }
          }
          • 1

          • 2

          • 3

          • 4

          • 5

          • 6

          • 7

          • 8

          • 9

          • 10

          • 11

          • 12

          • 13

          • 14

          • 15

          • 16

          • 17

          • 18

          • 19

          • 20

          • 21

          • 22

          • 23

          在JDK1.5中新增的JUC包就是建立在CAS之上的,相對于對于synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現(xiàn),他是一種基于忙等待的算法,依賴底層硬件的實現(xiàn),相對于鎖它沒有線程切換和阻塞的額外消耗,可以支持較大的并行度。

          雖然CAS沒有用到鎖,但是他在不停的自旋,會對CPU造成較大的執(zhí)行開銷,在生產(chǎn)中我們不建議使用,那么為什么我還會講呢,因為這是工作擰螺絲,面試造火箭的典型!你可以不用,但是你得知道,你說是吧。

          ——————END——————

          歡迎關(guān)注“Java引導(dǎo)者”,我們分享最有價值的Java的干貨文章,助力您成為有思想的Java開發(fā)工程師!

          瀏覽 48
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  人人澡人人爽人人精品 | 精品无码少妇一区二区三区 | a在线免费视频了 | 黄色成人网站在线观看 | 51无码人妻精品1国产蜜芽 |