1. 程式人生 > >如何把一個單例模式寫出花來

如何把一個單例模式寫出花來

1,懶載入模式

/**
 * 單例模式--懶載入模式
 *@author 萊格
 */
public class Singleton {
    //一個靜態的例項
    private static Singleton singleton;
    //私有建構函式
    private Singleton() {}
    //給出一個公共的訪問方法
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return
singleton; } }

懶載入在多執行緒的情況下測試:

public class Test {
    public static void main(String[] args) {
//      System.out.println(getInstance());
//      System.out.println(getInstance());
        //在單執行緒情況下,確實達到了單例的目的
        //[email protected]
        //[email protected]
        for (int i = 0; i < 100
; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"--"+Singleton.getInstance()); } },"t"+i).start(); } } }

在高併發情況下的測試結果,能看出來,不能保證建立物件的唯一性,當然在單執行緒環境下,它的物件是唯一的。

在高併發情況下的測試

針對不能實現在高併發環境下對外只提供一個物件的缺陷,提出了此種方案:

/**
 * 對上一種進行改造,讓其在高併發的情況下對外指提供一個物件,通過對整個方法加鎖的方式來實現。
 *@author 萊格
 */
public class Singleton2 {
    private static Singleton2 singleton2;
    //私有構造方法
    private Singleton2() {}
    //對外提供一個訪問的方法
    public static synchronized Singleton2 getInscance() {
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}
測試程式碼:

public class Test {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--"+Singleton2.getInscance());
                }
            },"t"+i).start();
        }
        long end = System.currentTimeMillis();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("耗時:"+(end-start));
    }
}

部分執行結果:

t0--com.bjsxt.sing.Singleton2@35b22911
t2--com.bjsxt.sing.Singleton2@35b22911
t1--com.bjsxt.sing.Singleton2@35b22911
t3--com.bjsxt.sing.Singleton2@35b22911
t4--com.bjsxt.sing.Singleton2@35b22911
t5--com.bjsxt.sing.Singleton2@35b22911
t6--com.bjsxt.sing.Singleton2@35b22911
t7--com.bjsxt.sing.Singleton2@35b22911
t8--com.bjsxt.sing.Singleton2@35b22911
t10--com.bjsxt.sing.Singleton2@35b22911
t9--com.bjsxt.sing.Singleton2@35b22911
t11--com.bjsxt.sing.Singleton2@35b22911
t12--com.bjsxt.sing.Singleton2@35b22911
t13--com.bjsxt.sing.Singleton2@35b22911
t14--com.bjsxt.sing.Singleton2@35b22911
t15--com.bjsxt.sing.Singleton2@35b22911
t16--com.bjsxt.sing.Singleton2@35b22911
t17--com.bjsxt.sing.Singleton2@35b22911
t18--com.bjsxt.sing.Singleton2@35b22911
t19--com.bjsxt.sing.Singleton2@35b22911
t20--com.bjsxt.sing.Singleton2@35b22911
....
此種耗時1100左右

由此可見,雖然實現了對外只提供一個物件,但是,他的缺陷是造成了無謂的等待,效能太低,針對此種缺陷,給出下一種方案。

//採用雙重檢查鎖的方式
public class Singleton2 {
    private static Singleton2 singleton2;
    //私有構造方法
    private Singleton2() {}
    //對外提供一個訪問的方法
    public static Singleton2 getInscance() {
        if (singleton2 == null) {
            synchronized (Singleton2.class) {
                if (singleton2 == null) {
                    singleton2 = new Singleton2();
                }
            }
        }
        return singleton2;
    }
}
耗時:950左右

同步只發生在單例還未建立的時候,一旦物件建立了就完全沒有必要在進行同步了,提高了效率。
那為什麼要在同步裡面再加個判斷呢?
首先,假設不加if,有n個執行緒進來看到例項為空,然後其中一個執行緒獲得鎖,進來之後建立物件,並且返回該物件的例項,並且釋放鎖,然後另外一個執行緒拿到鎖進來,由於沒有判斷物件是否為空,又在建立一次物件並返回,造成了多個建立多個物件的情況。

但是要是深入JVM底層,它可能是有問題的,因為在java虛擬機器在建立物件的時候是分了好幾步去執行的,也就是碩建立一個新的物件並非是原子操作的,在有些JVM中上述做法是沒有問題的,但是有些情況下是會造成莫名的錯誤。

虛擬機器建立物件的步驟:
1,分配記憶體
2,初始化構造器
3,將物件指向分配的記憶體地址
這種順序在上述雙重加鎖的方式是沒有問題的,因為這種情況下JVM是完成了整個物件的構造才將記憶體的地址交給了物件。但是如果2和3步驟是相反的(2和3可能是相反的是因為JVM會針對位元組碼進行調優,而其中的一項調優便是調整指令的執行順序),就會出現問題了。
因為這時將會先將記憶體地址賦值給物件,針對上述的雙重家加鎖,先將分配好的記憶體地址指給物件然後,後面的執行緒區請求getInstance方法時,會認為此物件已經被例項化了,直接返回一個引用,如果在初始化構造器之前,這個執行緒使用了當前物件,就會產生莫名其妙的錯誤。
單例建立模式是一個通用的程式設計習語。和多執行緒一起使用時,必需使用某種型別的同步。在努力建立更有效的程式碼時,Java 程式設計師們建立了雙重檢查鎖定習語,將其和單例建立模式一起使用,從而限制同步程式碼量。然而,由於一些不太常見的 Java 記憶體模型細節的原因,並不能保證這個雙重檢查鎖定習語有效。

它偶爾會失敗,而不是總失敗。此外,它失敗的原因並不明顯,還包含 Java 記憶體模型的一些隱祕細節。這些事實將導致程式碼失敗,原因是雙重檢查鎖定難於跟蹤。
所以此種方案還是有一定的缺陷, 所以有一種比較標準的單例模式。如下:

/**
 * 通過靜態內部類的方式建立單例
 *@author 萊格
 */
public class Singleton3 {
    private Singleton3(){}
        public static Singleton3 getInstance() {
        return InnerClassSingleton.singleton;
    }
    //通過靜態內部類的方式獲得物件

    private static class InnerClassSingleton{
        protected static Singleton3 singleton = new Singleton3();
    }
}   

我們在語言級別無法避免錯誤的發生,只有將該任務交給虛擬機器,所以才有以上一種i叫標準的單例模式,因為一個類的靜態屬性只會在第一次載入時初始化,所以我們無需擔心併發訪問的問題,在進行初始化一般的時候,別的執行緒 無法使用的,因為虛擬機器幫我們強行同步此過程,由於靜態變數只初始化了一次,所以singleton仍然是單例的。
但是,此種一定是安全的嗎?
no no no
其實是可以通過反射破壞的,具體如下:

public static void main(String[] args) {
        try {
            //通過反射去載入單例類
            Class<Singleton3> clazz = (Class<Singleton3>) Class.forName("com.bjsxt.sing.Singleton3");
            //獲得私有的構造方法
            Constructor<Singleton3> c = clazz.getDeclaredConstructor();
            //跳過訪問檢查
            c.setAccessible(true);
            //建立此單例的物件
            Singleton3 s = c.newInstance();
            //判斷兩個物件是否相等
            System.out.println(s == Singleton3.getInstance());//false
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
 通過以上的程式碼,我們可以觀察得出他們兩個並不是指向同一個記憶體地址,所以結果為false。

針對這個缺陷,提出了以下解決方案:

public class Singleton3 {
    private static boolean flag = false;
    private Singleton3() {
        synchronized (Singleton.class) {
            if (flag == false) {
                flag = !flag;
            }else {
                throw new RuntimeException("此例項已經被初始化!");
            }
        }
    }
    public static Singleton3 getInstance() {
        //flag = true;
        return InnerClassSingleton.singleton;
    }
    private static class InnerClassSingleton{
        protected static Singleton3 singleton = new Singleton3();
    }

    public static void main(String[] args) {
        try {
            //通過反射去載入單例類
            Class<Singleton3> clazz = (Class<Singleton3>) Class.forName("com.bjsxt.sing.Singleton3");
            //獲得私有的構造方法
            Constructor<Singleton3> c = clazz.getDeclaredConstructor();
            //跳過訪問檢查
            c.setAccessible(true);
            //建立此單例的物件
            Singleton3 s = c.newInstance();
            //判斷兩個物件是否相等
            //System.out.println(s);//第一次通過反射能載入到它
            System.out.println(s == Singleton.getInstance());//然而在判斷的時候就已經報錯
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

報錯資訊:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at com.bjsxt.sing.Singleton3.getInstance(Singleton3.java:26)
    at com.bjsxt.sing.Singleton3.main(Singleton3.java:43)
Caused by: java.lang.RuntimeException: 此例項已經被初始化!
    at com.bjsxt.sing.Singleton3.<init>(Singleton3.java:20)
    at com.bjsxt.sing.Singleton3.<init>(Singleton3.java:15)
    at com.bjsxt.sing.Singleton3$InnerClassSingleton.<clinit>(Singleton3.java:29)
    ... 2 more

然而通過此種方法完美的解決了反射呼叫問題,其核心原理就是:我們都知道類初始化的順序,先靜態,後動態。先屬性,後方法,當我們通過反射呼叫私有構造方法的時候,如果是第一次初始化,那麼,他就會建立物件,並且把flag的值置為true,接下來,我們在通過Singleton.getInstance()去呼叫的時候,因為他倆走的是不一樣的流程,它先初始化了自己的屬性,此時的flag為true,然後在初始化靜態內部類,然後在呼叫私有構造方法,此時的flag為true,所以直接執行else的內容,所以報錯。
假設先通過Singleton.getInstance()獲得物件,然後通過反射建立物件,也是同樣的結果。

但是在高併發的情況下,走正常的流程,即呼叫getInstance方法是不會出錯的。

synchronized (Singleton.class) {
            if (flag == false) {
                flag = !flag;
            }else {
                throw new RuntimeException("此例項已經被初始化!");
            }

還有一種更牛的寫法,就是採用列舉的方式實現:

/**
 * 採用列舉的方式獲得物件。
 *@author 萊格
 */
public enum EnumSingleton {
    INSTANCE;
    Demo instance;
    /**
     * 
     */
    private EnumSingleton() {
        instance = new Demo();
    }
    public Demo getInstance() {
        return instance;
    }
}
class Demo{
}

測試列舉:

/**
 * 測試列舉
 *@author 萊格
 */
public class TestEnum {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println(EnumSingleton.INSTANCE.getInstance());                   
                }
            }).start();
        }
    }
}

結果是在高併發的情況下依然也只是一個例項物件。下面我們分析一下此種寫法:
首先,在列舉中我們明確了構造方法限制為私有,在我們訪問列舉例項時會執行構造方法,同時每個列舉例項都是static final型別的,也就表明只能被例項化一次。在呼叫構造方法時,我們的單例被例項化。
也就是說,因為enum中的例項被保證只會被例項化一次,所以我們的INSTANCE也被保證例項化一次。
還有一種是註冊登記式的寫法:

/**
 * 採用註冊登記式的寫法
 *@author 萊格
 */
public class RegisterSingleton {
    //把物件都裝在容器裡邊,通過容器去取物件
    private static Map<String, RegisterSingleton> m = new HashMap<String,RegisterSingleton>();
    static {
        RegisterSingleton s = new RegisterSingleton();
        m.put(s.getClass().getName(), s);
    }
    //保護預設的構造器
    protected RegisterSingleton() {}
    //採用靜態工廠的方式獲得單例
    public static RegisterSingleton getInstance(String name) {
        if (name == null) {
            name = RegisterSingleton.class.getName();
        }
        if (m.get(name) == null) {
            try {
                m.put(name, (RegisterSingleton) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return m.get(name);
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {

                    System.out.println(RegisterSingleton.getInstance("com.bjsxt.sing.RegisterSingleton"));
                }
            }).start();
        }
    }
}

考率到應用場景,用靜態內部類或者列舉等的方式並不是適用於任何場景的,在SpringIOC容器裡就是通過此種方式獲得物件。

接下來還有一種模式:

餓載入模式:


/**
 * 餓載入模式
 *@author 萊格
 */
public class Singleton4 {
    private Singleton4() {}
    private static Singleton4 s = new Singleton4();
    public static Singleton4 getInstance() {
        return s;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                System.out.println( Singleton4.getInstance());
                }
            }).start();
        }
    }
}

他的缺陷就是記憶體開銷太大,每個執行緒過來都得開闢一個空間,所以這種方式不太推薦,理論上通過呼叫getInstance()方法,雖然執行緒安全,但實際上它依然是不安全的,依然可以通過其他方式破壞。那就是通過序列化對他進行破壞。來看程式碼:

/**
 * 餓載入模式通過序列化對它進行破壞
 *@author 萊格
 */
public class Singleton4 implements Serializable{
    private Singleton4() {}
    private static Singleton4 s = new Singleton4();
    public static Singleton4 getInstance() {
        return s;
    }
    public static void main(String[] args) {
//      for (int i = 0; i < 10; i++) {
//          new Thread(new Runnable() {
//              
//              @Override
//              public void run() {
//              System.out.println( Singleton4.getInstance());
//              }
//          }).start();
//      }
        Singleton4.writeInstance();//寫道硬碟上
        System.out.println(Singleton4.getInstance());//列印呼叫getInstance返回的物件地址
        System.out.println(Singleton4.readInstance());//列印呼叫readInstacne返回的物件地址
        System.out.println(Singleton4.getInstance() == Singleton4.readInstance());//false
    }
    //答物件寫到硬碟上
    public static void writeInstance() {
        try {
            Singleton4 instance = Singleton4.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./obj.txt"));
            oos.writeObject(instance);

            oos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
    //把物件從硬碟給讀出來
    public static Singleton4 readInstance() {
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./obj.txt"));
            Singleton4 instance = (Singleton4) ois.readObject();
            ois.close();
            return instance;
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
    }
}

那既然這樣要如何解決這個問題呢?
看解決方案:

public class Singleton4 implements Serializable{
    private Singleton4() {}
    private static Singleton4 s = new Singleton4();
    public static Singleton4 getInstance() {
        return s;
    }
    //解決因序列化問題而產生的物件不一致的問題
    //readResolve的最主要應用場合就是單例、列舉型別的保護性恢復!
    private Object readResolve() {
        return s;
    }
    public static void main(String[] args) {
        Singleton4.writeInstance();//寫到硬碟上
        System.out.println(Singleton4.getInstance());//列印呼叫getInstance返回的物件地址
        System.out.println(Singleton4.readInstance());//列印呼叫readInstacne返回的物件地址
        System.out.println(Singleton4.getInstance() == Singleton4.readInstance());//最終列印結果是true
    }
    //答物件寫到硬碟上
    public static void writeInstance() {
        try {
            Singleton4 instance = Singleton4.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./obj.txt"));
            oos.writeObject(instance);

            oos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
    //把物件從硬碟給讀出來
    public static Singleton4 readInstance() {
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./obj.txt"));
            Singleton4 instance = (Singleton4) ois.readObject();
            ois.close();
            return instance;
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
    }
}

重點就在這裡:
//readResolve的最主要應用場合就是單例、列舉型別的保護性恢復!
private Object readResolve() {
return s;
}

寫了這麼多,是時候來一發總結了
第一點,各種問題的解決方案:
首先執行緒安全的問題,內部類是完美的。
其次,用反射破壞的問題,通過在私有構造方法中新增靜態內部載入判斷
然後就是,序列化的問題,重寫readResolve方法完美解決了。

第二點,介紹了每種模式的優缺點並且還引出靜態工廠模式建立單例在spring ioc 裡面的應用。

終於寫完了~~~

相關推薦

如何一個模式

1,懶載入模式 /** * 單例模式--懶載入模式 *@author 萊格 */ public class Singleton { //一個靜態的例項 private static Singleton singleton; /

題二:一個模式

餓漢 ret gets col ets int ati sta println /** * 2、寫一個單例模式 */ public class Test2 { public static void main(String[] args) {

能否一個模式,並且保證例項的唯一性?

這算是Java一個比較核心的問題了,面試官期望你能知道在寫單例模式時應該對例項的初始化與否進行雙重檢查。記住對例項的宣告使用Volatile關鍵字,以保證單例模式是執行緒安全的。下面是一段示例,展示瞭如何用一種執行緒安全的方式實現了單例模式: public class

面試:一個模式,足以你秒成渣

去面試(對,又去面試) 問:單例模式瞭解吧,來,拿紙和筆寫一下單例模式。 我心想,這TM不是瞧不起人嗎?我程式設計十年,能不知道單例模式。 答:(.net 平臺下)單例模式有兩種寫法: 第一種:飢餓模式,關鍵點,static readonly public static readonly Singleton

練習創建一個模式例子

eve jpg closed div 使用 htm stop http pla 昨天有寫過一個單例模式的練習。《單例模式(C#)》 寫得較為復雜,今天再使用另外的方法來實現: class Au { private static

一個模式的實現

歡迎大家點評,這是個人寫的一個單例模板 #pragma once //單例模板 #include <windows.h> class CrsLock { public: CrsLock(){ InitializeCriticalSection(&m_cr

模式MySQL model類,簡單的增、刪、改、查

單例模式的用途,可用於資料庫操作 <?php Class Db { static private $whe;//條件 static private $tab;//表名 static private $lim;//分段變數 static private $ord

C++實現一個模式(懶漢與餓漢)

單例模式的特點: 1、一個類只能有一個例項。 2、一個類必須自己建立自己的唯一例項。 3、一個類必須給所有其他物件提供這一例項。 單例模式的實現: 1、將建構函式宣告為private防止被外部

【JS 設計模式 】用模式(Singleton)封裝對資料的增刪除改查

單例模式單例模式的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類只有一個例項單例模式最初的定義出現於《設計模式》(艾迪生維斯理, 1994):“保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。”單例模式定義:“一個類有且僅有一個例項,並且自行

模板方法模式 + 觀察者模式 + 簡單工廠模式 + 模式實現一個簡單的數據表讀

private 數據庫鏈 obs imp 通知 model 數據表 ring pri 實現功能: 對數據表的讀要緩存起來,對數據表的寫需要清除緩存. 數據表根據屬性字段來決定是否緩存 可以更換數據庫鏈接方式,比如可以隨時更換為mysql或mysqli() 當插入數據時給出一

一個Singleton(模式

Singleton模式主要作用是保證在Java應用程式中,一個類Class只有一個例項存在。 一般Singleton模式通常有幾種種形式:第一一種形式:定義一一個類,它的建構函式為private的,它有一個static的private的該類變數,在類初始化時例項話,通過

如何正確地模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章

載入一個類時,其內部類是否同時被載入?引申模式的另一種實現方式...

載入一個類時,其內部類是否同時被載入?下面我們做一個實驗來看一下。public class Outer { static { System.out.println("load outer class..."); } //靜態內部類 sta

載入一個類時,其內部類是否同時被載入?引申模式的另一種實現方式

 載入一個類時,其內部類是否同時被載入?下面我們做一個實驗來看一下。  Java程式碼   1.    public class Outer {   2.        static {   3.            System.out.println("load o

如何正確地模式(懶漢式和餓漢式寫法)

本文轉自大神:伍翀 原文連結 單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什

怎麼一個氣泡排序,遞迴,模式的使用

package 氣泡排序; import java.util.Arrays; /** * 氣泡排序改進版 * @author zjn * */ public class BubbleSort1 { public static void Bubbl

Java如何正確地模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。 懶漢式,執行緒不安全 當被

JAVA的模式(用java一個singleton的例子)。

程式碼如下: package test; public class SingleObject {// 建立 SingleObject 的一個物件private static SingleObject

【J2EE學習】(四)如何正確地模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。

一個執行緒安全的模式

1. 錯誤的寫法:雖然用到了volatile,但是volatile只能保證可見性,並不保證原子性public class wrongsingleton { private static vol