1. 程式人生 > >【轉載】Java實現單例的5種方式

【轉載】Java實現單例的5種方式

Java實現單例的5種方式

【轉載】出處:https://blog.csdn.net/u014672511/article/details/79774847

1. 什麼是單例模式

單例模式指的是在應用整個生命週期內只能存在一個例項。單例模式是一種被廣泛使用的設計模式。他有很多好處,能夠避免例項物件的重複建立,減少建立例項的系統開銷,節省記憶體。

2. 單例模式和靜態類的區別

首先理解一下什麼是靜態類,靜態類就是一個類裡面都是靜態方法和靜態field,構造器被private修飾,因此不能被例項化。Math類就是一個靜態類。

知道了什麼是靜態類後,來說一下他們兩者之間的區別:

1)首先單例模式會提供給你一個全域性唯一的物件,靜態類只是提供給你很多靜態方法,這些方法不用建立物件,通過類就可以直接呼叫;

2)單例模式的靈活性更高,方法可以被override,因為靜態類都是靜態方法,所以不能被override;

3)如果是一個非常重的物件,單例模式可以懶載入,靜態類就無法做到;

那麼時候時候應該用靜態類,什麼時候應該用單例模式呢?首先如果你只是想使用一些工具方法,那麼最好用靜態類,靜態類比單例類更快,因為靜態的繫結是在編譯期進行的。如果你要維護狀態資訊,或者訪問資源時,應該選用單例模式。還可以這樣說,當你需要面向物件的能力時(比如繼承、多型)時,選用單例類,當你僅僅是提供一些方法時選用靜態類。

3.如何實現單例模式

1. 餓漢模式

所謂餓漢模式就是立即載入,一般情況下再呼叫getInstancef方法之前就已經產生了例項,也就是在類載入的時候已經產生了。這種模式的缺點很明顯,就是佔用資源,當單例類很大的時候,其實我們是想使用的時候再產生例項。因此這種方式適合佔用資源少,在初始化的時候就會被用到的類。

class SingletonHungary {
    private static SingletonHungary singletonHungary = new SingletonHungary();
    //將構造器設定為private禁止通過new進行例項化
    private SingletonHungary() {

    }
    public static SingletonHungary getInstance() {
        return singletonHungary;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2. 懶漢模式

懶漢模式就是延遲載入,也叫懶載入。在程式需要用到的時候再建立例項,這樣保證了記憶體不會被浪費。針對懶漢模式,這裡給出了5種實現方式,有些實現方式是執行緒不安全的,也就是說在多執行緒併發的環境下可能出現資源同步問題。

首先第一種方式,在單執行緒下沒問題,在多執行緒下就出現問題了。

// 單例模式的懶漢實現1--執行緒不安全
class SingletonLazy1 {
    private static SingletonLazy1 singletonLazy;

    private SingletonLazy1() {

    }

    public static SingletonLazy1 getInstance() {
        if (null == singletonLazy) {
            try {
                // 模擬在建立物件之前做一些準備工作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singletonLazy = new SingletonLazy1();
        }
        return singletonLazy;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

我們模擬10個非同步執行緒測試一下:

public class SingletonLazyTest {

    public static void main(String[] args) {

        Thread2[] ThreadArr = new Thread2[10];
        for (int i = 0; i < ThreadArr.length; i++) {
            ThreadArr[i] = new Thread2();
            ThreadArr[i].start();
        }
    }

}

// 測試執行緒
class Thread2 extends Thread {
    @Override
    public void run() {
        System.out.println(SingletonLazy1.getInstance().hashCode());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

執行結果:

124191239
124191239
872096466
1603289047
1698032342
1913667618
371739364
124191239
1723650563
367137303
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到他們的hashCode不都是一樣的,說明在多執行緒環境下,產生了多個物件,不符合單例模式的要求。

那麼如何使執行緒安全呢?第二種方法,我們使用synchronized關鍵字對getInstance方法進行同步。

// 單例模式的懶漢實現2--執行緒安全
// 通過設定同步方法,效率太低,整個方法被加鎖
class SingletonLazy2 {
    private static SingletonLazy2 singletonLazy;

    private SingletonLazy2() {

    }

    public static synchronized SingletonLazy2 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在建立物件之前做一些準備工作
                Thread.sleep(1000);
                singletonLazy = new SingletonLazy2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return singletonLazy;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

使用上面的測試類,測試結果:

1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到,這種方式達到了執行緒安全。但是缺點就是效率太低,是同步執行的,下個執行緒想要取得物件,就必須要等上一個執行緒釋放,才可以繼續執行。

那我們可以不對方法加鎖,而是將裡面的程式碼加鎖,也可以實現執行緒安全。但這種方式和同步方法一樣,也是同步執行的,效率也很低。

// 單例模式的懶漢實現3--執行緒安全
// 通過設定同步程式碼塊,效率也太低,整個程式碼塊被加鎖
class SingletonLazy3 {

    private static SingletonLazy3 singletonLazy;

    private SingletonLazy3() {

    }

    public static SingletonLazy3 getInstance() {
        try {
            synchronized (SingletonLazy3.class) {
                if (null == singletonLazy) {
                    // 模擬在建立物件之前做一些準備工作
                    Thread.sleep(1000);
                    singletonLazy = new SingletonLazy3();
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}
  • 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

我們來繼續優化程式碼,我們只給建立物件的程式碼進行加鎖,但是這樣能保證執行緒安全麼?

// 單例模式的懶漢實現4--執行緒不安全
// 通過設定同步程式碼塊,只同步建立例項的程式碼
// 但是還是有執行緒安全問題
class SingletonLazy4 {

    private static SingletonLazy4 singletonLazy;

    private SingletonLazy4() {

    }

    public static SingletonLazy4 getInstance() {
        try {
            if (null == singletonLazy) {        //程式碼1
                // 模擬在建立物件之前做一些準備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy4.class) {
                    singletonLazy = new SingletonLazy4(); //程式碼2
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}
  • 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

我們來看一下執行結果:

1210004989
1425839054
1723650563
389001266
1356914048
389001266
1560241484
278778395
124191239
367137303
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

從結果看來,這種方式不能保證執行緒安全,為什麼呢?我們假設有兩個執行緒A和B同時走到了‘程式碼1’,因為此時物件還是空的,所以都能進到方法裡面,執行緒A首先搶到鎖,建立了物件。釋放鎖後執行緒B拿到了鎖也會走到‘程式碼2’,也建立了一個物件,因此多執行緒環境下就不能保證單例了。

讓我們來繼續優化一下,既然上述方式存在問題,那我們在同步程式碼塊裡面再一次做一下null判斷不就行了,這種方式就是我們的DCL雙重檢查鎖機制。

//單例模式的懶漢實現5--執行緒安全
//通過設定同步程式碼塊,使用DCL雙檢查鎖機制
//使用雙檢查鎖機制成功的解決了單例模式的懶漢實現的執行緒不安全問題和效率問題
//DCL 也是大多數多執行緒結合單例模式使用的解決方案
class SingletonLazy5 {

    private static SingletonLazy5 singletonLazy;

    private SingletonLazy5() {

    }

    public static SingletonLazy5 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在建立物件之前做一些準備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy5.class) {
                    if(null == singletonLazy) {
                        singletonLazy = new SingletonLazy5();
                    }
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}
  • 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
  • 28
  • 29

執行結果:

124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我們可以看到DCL雙重檢查鎖機制很好的解決了懶載入單例模式的效率問題和執行緒安全問題。這也是我們最常用到的方式。

3. 靜態內部類

我們也可以使用靜態內部類實現單例模式,程式碼如下:

//使用靜態內部類實現單例模式--執行緒安全
class SingletonStaticInner {
    private SingletonStaticInner() {

    }
    private static class SingletonInner {
        private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
    }
    public static SingletonStaticInner getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return SingletonInner.singletonStaticInner;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可以看到使用這種方式我們沒有顯式的進行任何同步操作,那他是如何保證執行緒安全呢?和餓漢模式一樣,是靠JVM保證類的靜態成員只能被載入一次的特點,這樣就從JVM層面保證了只會有一個例項物件。那麼問題來了,這種方式和餓漢模式又有什麼區別呢?不也是立即載入麼?實則不然,載入一個類時,其內部類不會同時被載入。一個類被載入,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被呼叫時發生。

可以說這種方式是實現單例模式的最優解。

4. 靜態程式碼塊

這裡提供了靜態程式碼塊實現單例模式。這種方式和第一種類似,也是一種餓漢模式。

//使用靜態程式碼塊實現單例模式
class SingletonStaticBlock {
    private static SingletonStaticBlock singletonStaticBlock;
    static {
        singletonStaticBlock = new SingletonStaticBlock();
    }
    public static SingletonStaticBlock getInstance() {
        return singletonStaticBlock;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5. 序列化與反序列化

LZ為什麼要提序列化和反序列化呢?因為單例模式雖然能保證執行緒安全,但在序列化和反序列化的情況下會出現生成多個物件的情況。執行下面的測試類,

public class SingletonStaticInnerSerializeTest {

    public static void main(String[] args) {
        try {
            SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance();
            System.out.println(serialize.hashCode());
            //序列化
            FileOutputStream fo = new FileOutputStream("tem");
            ObjectOutputStream oo = new ObjectOutputStream(fo);
            oo.writeObject(serialize);
            oo.close();
            fo.close();
            //反序列化
            FileInputStream fi = new FileInputStream("tem");
            ObjectInputStream oi = new ObjectInputStream(fi);
            SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject();
            oi.close();
            fi.close();
            System.out.println(serialize2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

//使用匿名內部類實現單例模式,在遇見序列化和反序列化的場景,得到的不是同一個例項
//解決這個問題是在序列化的時候使用readResolve方法,即去掉註釋的部分
class SingletonStaticInnerSerialize implements Serializable {

    /**
     * 2018年03月28日
     */
    private static final long serialVersionUID = 1L;

    private static class InnerClass {
        private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize();
    }

    public static SingletonStaticInnerSerialize getInstance() {
        return InnerClass.singletonStaticInnerSerialize;
    }

//  protected Object readResolve() {
//      System.out.println("呼叫了readResolve方法");
//      return InnerClass.singletonStaticInnerSerialize;
//  }
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

可以看到:

865113938
1078694789
  • 1
  • 2

結果表明的確是兩個不同的物件例項,違背了單例模式,那麼如何解決這個問題呢?解決辦法就是在反序列化中使用readResolve()方法,將上面的註釋程式碼去掉,再次執行:

865113938
呼叫了readResolve方法
865113938
  • 1
  • 2
  • 3

問題來了,readResolve()方法到底是何方神聖,其實當JVM從記憶體中反序列化地”組裝”一個新物件時,就會自動呼叫這個 readResolve方法來返回我們指定好的物件了, 單例規則也就得到了保證。readResolve()的出現允許程式設計師自行控制通過反序列化得到的物件。

相關推薦

轉載Java實現5方式

Java實現單例的5種方式【轉載】出處:https://blog.csdn.net/u014672511/article/details/797748471. 什麼是單例模式單例模式指的是在應用整個生命週期內只能存在一個例項。單例模式是一種被廣泛使用的設計模式。他有很多好處,

Java中用模式有什麽好處

別了 通過 詳細 線程同步 請求 占用 解決辦法 每次 耦合 Java Singleton模式主要作用是保證在Java應用程序中,一個類Class只有一個實例存在。 使用Singleton的好處還在於可以節省內存,因為它限制了實例的個數,有利於Java垃圾回收(garb

轉載form表的兩提交方式,submit和button的用法

按鈕 type ssid login false tex .get ons 轉載 1.當輸入用戶名和密碼為空的時候,需要判斷。這時候就用到了校驗用戶名和密碼,這個需要在jsp的前端頁面寫;有兩種方法,一種是用submit提交。一種是用button提交。方法一:在jsp的前端

第11天Java模式、介面以及Object類常用的方法

1 單例模式 1.1 醉漢式 1.2 懶漢式 2 介面(interface) 3 Object類常用的方法 3.1 clone() 3.2 finalize()

轉載Java動態代理之JDK實現和CGlib實現(簡單易懂)

      原文地址:http://www.cnblogs.com/ygj0930/p/6542259.html       一:代理模式(靜態代理)           代理模式是常用設計模式的一種,我們在軟體設計時常用的代理一般是指靜態代理,也就是在程式碼中顯式指定的

專欄 - Java語言下單模式的實現

Java語言下單例模式的實現 單例模式,即整個程式中某個物件只能被例項化一次,而在多執行緒環境下,普通懶漢式單例模式將無法滿足要求,這裡主要介紹幾種方法

深思列舉實現原理

單例的列舉實現在《Effective Java》中有提到,因為其功能完整、使用簡潔、無償地提供了序列化機制、在面對複雜的序列化或者反射攻擊時仍然可以絕對防止多次例項化等優點,單元素的列舉型別被作者認為

溫故而知新(java實現)模式的七寫法

反序 防止 代碼 工作 html 我想 變種 evel 才會 第一種(懶漢,線程不安全): Java代碼 public class Singleton { private static Singleton instance; private S

轉載Java中Comparable和Comparator比較

import 比較器 todo itl 復制代碼 ack div array open 【本文轉自】http://www.cnblogs.com/skywang12345/p/3324788.html Comparable 簡介 Comparable 是排序接口。 若一

轉載JAVA多線程讀取、操作List集合

線程 nbsp static 一點 stat lang 素數 param 應用 本文轉載自:http://blog.csdn.net/wang1989cs/article/details/47663565 import java.util.ArrayList; impor

轉載java 客戶端鏈接不上redis解決方案 (jedis)

主機 rom number table 出現 gin 現在 start http 本文出自:http://blog.csdn.net/lulidaitian/article/details/51946169 出現問題描述: 1.Could not get a resou

轉載java 獲取路徑的各種方法

strong filepath 文件路徑 etc dex 讀取 workspace for class 轉載只供個人學習參考,查看請前往原出處:http://www.cnblogs.com/guoyuqiangf8/p/3506768.html 主要方法有: (1)、req

opencvJava實現的opencv3.x版本後Highhui報錯

-a sun div fan let 版本 ava line 報錯 隨筆為博主原創,如需轉載,請註明出處。 opencv3.x以後Highgui不再使用,用Imgcodecs代替,引入import org.opencv.imgcodecs.Imgcode

轉載Java DecimalFormat 用法

最快 public oid get div 科學計數 科學 符號 一個 轉載只供個人學習參考,以下查看請前往原出處:http://blog.csdn.net/wangchangshuai0010/article/details/8577982 我們經常要將數字進行格式化,比

轉載JAVA中IO流操作的基本規律總結

reader 對象 作文 若是 gpo 是否 目的 io流 基本 流操作的基本規律:三個明確 1.明確源和目的; 源:輸入流即讀取流 InputStream,Reader 目的:輸出流即寫入流 OutputStream, Writer 2.明確操作的數據是

轉載JAVA學習路線二

get rocket 原理 aop 二次 三次握手 active AD cpu JAVA學習路線二------------高階面試 作者:Java高級進階鏈接:https://zhuanlan.zhihu.com/p/35301291來源:知乎著作權歸作者所有。商業轉載請聯

轉載JAVA字符串格式化-String.format()的使用

對數 print 英文月份 兩種 local 之前 width 標記 散列碼 常規類型的格式化 String類的format()方法用於創建格式化的字符串以及連接多個字符串對象。熟悉C語言的同學應該記得C語言的sprintf()方法,兩者有類似之處。format()方法有兩

轉載Java基本型別的Writable封裝

Java基本型別的Writable封裝 目前Java基本型別對應的Writable封裝如表所示 所有這些Writable類都繼承自WritableComparable。也就是說,它們是可比較的。 同時,它們都有get()和set()方法,用於獲得和設定封裝的值。 對整形(int和long

轉載Java 中帶參無返回、帶參帶返回值、方法的重載

語法 ble 數組 實現 執行 愛慕 包含 參數 com 一、 有時方法的執行需要依賴於某些條件,換句話說,要想通過方法完成特定的功能,需要為其提供額外的信息才行。例如,現實生活中電飯鍋可以實現“煮飯”的功能,但前提是我們必須提供食材,如果我們什麽都不提供,那就真是的“巧婦

轉載Java 中帶參無返回、帶參帶返回值、方法的過載

一、 有時方法的執行需要依賴於某些條件,換句話說,要想通過方法完成特定的功能,需要為其提供額外的資訊才行。例如,現實生活中電飯鍋可以實現“煮飯”的功能,但前提是我們必須提供食材,如果我們什麼都不提供,那就真是的“巧婦難為無米之炊”了。我們可以通過在方法中加入引數列表接收外部傳入的資料資訊,引數可以是任意的基