Java五種單例模式與線程安全

懶漢式程序員

顧名思義,lazy loading(延遲加載,一說懶加載),在須要的時候才建立單例對象,而不是隨着軟件系統的運行或者當類被加載器加載的時候就建立。當單例類的建立或者單例對象的存在會消耗比較多的資源,經常採用lazy loading策略。這樣作的一個明顯好處是提升了軟件系統的效率,節約內存資源。下面咱們看看最簡單的懶漢單例模式:安全

代碼1-1多線程

?ide

1函數

2性能

3測試

4spa

5.net

6線程

7

8

9

10

11

12

13

14

15

16

public class Singleton {

    private static Singleton singleton = null;    //私有的、類型爲Singleton自身的靜態成員變量

     

    //構造方法被設爲私有,防止外部使用new來建立對象,破壞單例

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    //公有的靜態方法,供外部調用來獲取單例對象

    public static Singleton getInstance(){

        if(singleton == null){    //第一次調用該方法時,建立對象。

            singleton = new Singleton();

        }

        return singleton;

    }

}

 

在單線程環境下,屢次調用getInstance()方法得到的Singleton對象均爲同一個對象,單例模式實現成功。然而,在更多時候,軟件系統工做於多線程環境下,所以不得不考慮線程安全的問題。

現有多線程測試程序以下:

代碼1-2

?

1

2

3

4

5

6

7

8

9

10

public class TestThread {

    public static void main(String[] args) {

        Runnable run = () -> Singleton.getInstance();    //建立實現了Runnable接口的匿名類

 

        for(int i = 0; i < 50; i++){

            Thread thread = new Thread(run);

            thread.start();

        }

    }

}

 

代碼中先建立了一個實現了Runnable接口的匿名類對象run,而後用for循環建立並啓動50個線程,其中一次運行結果以下:

?

1

2

3

4

構造函數被調用

構造函數被調用

構造函數被調用

構造函數被調用

 

 

顯然,Singleton的構造方法不止一次被調用,也就是說,Singleton存在四個實例對象,這違背了單例模式的初衷。這個實驗說明,簡單的懶漢式在多線程環境下不是線程安全的。有人提出在getInstance()方法上同步鎖,可是鎖住一整個方法可能粒度過大,不利於效率。既然鎖方法不太好,那麼鎖代碼呢?下面咱們再看看兩個例子:

代碼1-3

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class Singleton {

    private static Singleton singleton = null;    

     

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    public static Singleton getInstance(){

        if(singleton == null){

            synchronized(Singleton.class){

                singleton = new Singleton();

            }

        }

        return singleton;

    }

}

 

代碼段1-3的getInstance()方法裏,在判空語句後上鎖,把singleton = new Singleton()語句鎖住了,這樣作看似解決了線程安全問題,其實否則。設現有線程A和B,在t1時刻線程A和B均已經過判空語句但都未取得鎖資源;t2時刻時,A先取得鎖資源進入臨界區(被鎖的代碼塊),執行new操做建立實例對象,而後退出臨界區,釋放鎖資源。t3時刻,B取得被A釋放的鎖資源進入臨界區,執行new操做建立實例對象,而後退出臨界區,釋放鎖資源。明顯地,Singleton被實例化兩次。因此,如代碼段1-3這樣寫也不能保證線程安全。

代碼1-4

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class Singleton {

    private static Singleton singleton = null;    

     

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    public static Singleton getInstance(){

            synchronized(Singleton.class){

                if(singleton == null){

                singleton = new Singleton();

            }

        }

        return singleton;

    }

}

 

    代碼段1-4把代碼鎖放在了判空語句前,這樣作避免了代碼段1-3的問題,然而這樣作相似於在方法簽名上加上synchronized關鍵字,會影響程序效率。由於當有多個線程幾乎同時訪問getInstance方法時,多個線程必須有次序地進入方法內,這樣致使了若干個線程須要耗費等待進入臨界區(被鎖住的代碼塊)的時間。基於此,有人提出了雙重校驗鎖式。

 

 

雙重校驗鎖DCL(double checked locking)

雙重校驗鎖式(也有人把雙重校驗鎖式和懶漢式歸爲一類)分別在代碼鎖先後進行判空校驗,避免了多個有機會進入臨界區的線程都建立對象,同時也避免了代碼段1-4後來線程在先來線程建立對象後但仍未退出臨界區的狀況下等待。雙重校驗鎖代碼以下:

代碼2-1

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public class Singleton{

        private volatile static Singleton singleton = null;    //注意此處加上了volatile關鍵字

     

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    public static Singleton getInstance(){

        if(singleton == null){

            synchronized(Singleton.class){

                if(singleton == null){

                    singleton = new Singleton();

                    //return singleton;    //有人提議在此處進行一次返回

                }

                //return singleton;    //也有人提議在此處進行一次返回

            }

        }

        return singleton;

    }

}

 

經屢次試驗說明,雙重校驗鎖式是線程安全的。然而,在JDK1.5之前,DCL是不穩定的,有時也可能建立多個實例,在1.5之後開始提供volatile關鍵字修飾變量來達到穩定效果。

 

 

餓漢式

單例模式的餓漢式,在定義自身類型的成員變量時就將其實例化,使得在Singleton單例類被系統(姑且這麼說)加載時就已經被實例化出一個單例對象,從而一勞永逸地避免了線程安全的問題。代碼以下:

代碼3-1

?

1

2

3

4

5

6

7

8

9

10

11

public class Singleton{

        private static Singleton singleton = new Singleton();    //在定義變量時就將其實例化

     

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    public static Singleton getInstance(){

        return singleton;

    }

}

 

使用多線程測試代碼1-2進行測試,單例模式成功實現。

我想應該有朋友對餓漢式單例在什麼時候被實例化感興趣。能夠編寫以下簡單測試代碼:

代碼3-2

?

1

2

3

4

5

public class TestThread {

    public static void main(String[] args) {

        Class.forName("Singleton");

    }

}

 

運行這段代碼後能夠看到控制檯有「構造函數被調用」字符串輸出,說明在ClassLoader加載Singleton類時,餓漢式單例就被建立。

雖然餓漢式單例是線程安全的,但也有其不足之處。餓漢式單例在類被加載時就建立單例對象而且長駐內存,無論你需不須要它;若是單例類佔用的資源比較多,就會下降資源利用率以及程序的運行效率。有一種更高級的單例模式則很好地解決了這個問題——靜態內部類。

 

 

IoDH(Initialization Demand Holder)——經過靜態內部類實現線程安全的單例模式

靜態內部類式在Singleton類內部定義了一個靜態的內部類,在該內部類裏建立Singleton的單例對象。咱們先看代碼:

代碼4-1

?

1

2

3

4

5

6

7

8

9

10

11

12

13

public class Singleton {

    private Singleton(){

        System.out.println("構造函數被調用");

    }

     

    public static Singleton getInstance(){

        return SingletonHolder.instance;

    }

     

    private static class SingletonHolder{

        private static Singleton instance = new Singleton();

    }

}

 

靜態內部類式和餓漢式同樣,一樣利用了ClassLoader的機制保證了線程安全;不一樣的是,餓漢式在Singleton類被加載時(從代碼段3-2的Class.forName可見)就建立了一個實例對象,而靜態內部類即便Singleton類被加載也不會建立單例對象,除非調用裏面的getInstance()方法。由於當Singleton類被加載時,其靜態內部類SingletonHolder沒有被主動使用。只有當調用getInstance方法時,纔會裝載SingletonHolder類,從而實例化單例對象。

這樣,經過靜態內部類的方法就實現了lazy loading,很好地將懶漢式和餓漢式結合起來,既實現延遲加載,保證系統性能,也能保證線程安全。

 

然而,對於上述四種方式的單例模式,若是你的Singleton類實現了Serializable序列化接口,那麼可能會被序列化生成多個實例,由於readObject()方法一直返回一個新的對象:

?

1

2

3

4

5

6

ByteArrayOutputStream baos  = new ByteArrayOutputStream();

ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(singleton);

  

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));

Singleton singleton=  (Singleton) ois.readObject();

 

這種狀況能夠經過在Singleton類添加readResolve()方法來解決:

?

1

2

3

4

private Object readResolve() {

        System.out.println("readResolve()被調用");

        return getInstance();

}

 

可是這種解決方案雖解決了序列化的問題,可是沒法避免被反射。下面還有一種枚舉單例,寫法簡單,還能夠避免序列化、反射的問題。

 

 

枚舉單例

上面說到的靜態內部類方式不失爲一個高級的單例模式實現。但若是開發要求更嚴格一些,好比你的Singleton類實現了序列化,又或者想避免經過反射來破解單例模式的話,單例模式還能夠有另外一種形式。那就是枚舉單例。枚舉類型在JDK1.5被引進。這種方式也是《Effective Java》做者Josh Bloch 提倡的方式,它不只能避免多線程的問題,並且還能防止反序列化從新建立新的對象、防止被反射攻擊。代碼以下:

代碼5-1

?

1

2

3

4

5

6

7

8

9

10

11

public enum EnumSingleton {

    INSTANCE{

        @Override

        protected void work() {

            System.out.println("你好,是我!");

        }

         

    };

     

    protected abstract void work();    //單例須要進行操做(也能夠不寫成抽象方法)

}

 

在外部,能夠經過EnumSingleton.INSTANCE.work()來調用work方法。默認的枚舉實例的建立是線程安全的,可是實例內的各類方法則須要程序員來保證線程安全。總的來講,使用枚舉單例模式,有三個好處:1.實例的建立線程安全,確保單例。2.防止被反射建立多個實例。3.沒有序列化的問題。

相關文章
相關標籤/搜索