1、什么是單例模式?
采取一定的辦法保證在整個軟件系統中,單例模式確保對於某個類只能存在一個實例。有如下三個特點:
①、單例類只能有一個實例
②、單例類必須自己創建自己的實例
③、單例類必須提供外界獲取這個實例的方法
2、單例類的設計思想(Singleton)
①、外界不能創建這個類的實例,那么必須將構造器私有化。
public class Singleton { //構造器私有化 private Singleton(){ } }
②、單例類必須自己創建自己的實例,不能允許在類的外部修改內部創建的實例,所以將這個實例用 private 聲明。為了外界能訪問到這個實例,我們還必須提供 get 方法得到這個實例。因為外界不能 new 這個類,所以我們必須用 static 來修飾字段和方法。
//在類的內部自己創建實例 private static Singleton singleton = new Singleton(); //提供get 方法以供外界獲取單例 public Singleton getInstance(){ return singleton; }
3、單例模式之餓漢模式
public class Singleton { //構造器私有化 private Singleton(){ } //在類的內部自己創建實例 private static Singleton singleton = new Singleton(); //提供get 方法以供外界獲取單例 public static Singleton getInstance(){ return singleton; } }
測試:
public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1.equals(s2)); //true }
這種模式避免了多線程的同步問題,不過在 類裝載的時候就進行了實例化,有可能這個實例化過程很長,那么就會加大類裝載的時間;有可能這個實例現階段根本用不到,那么創建了這個實例,也會浪費內存。沒有達到 lazy-loading 的效果。
4、單例模式之懶漢模式(線程不安全)
//懶漢模式 public class Singleton { //構造器私有化 private Singleton(){ } //在類的內部自己創建實例的引用 private static Singleton singleton = null; //提供get 方法以供外界獲取單例 public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }
這種方法達到了 lazy-loading 的效果,即我們在第一次需要得到這個單例的時候,才回去創建它的實例,以后再需要就可以不用創建,直接獲取了。但是這種設計在多線程的情況下是不安全的。
我們可以創建兩個線程來看看這種情況:
public class ThreadSingleton extends Thread{ @Override public void run() { try { System.out.println(Singleton.getInstance()); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadSingleton s1 = new ThreadSingleton(); s1.start(); //com.ys.pattern.Singleton@5994a1e9 ThreadSingleton s2 = new ThreadSingleton(); s2.start(); //com.ys.pattern.Singleton@40dea6bc } }
很明顯:最后輸出結果的兩個實例是不同的。這便是線程安全問題。那么怎么解決這個問題呢?
參考這篇博客:Java多線程同步:http://www.cnblogs.com/ysocean/p/6883729.html
5、單例模式之懶漢模式(線程安全)
這里我們采用同步代碼塊來達到線程安全
//懶漢模式線程安全 public class Singleton { //構造器私有化 private Singleton(){ } //在類的內部自己創建實例的引用 private static Singleton singleton = null; //提供get 方法以供外界獲取單例 public static Singleton getInstance() throws Exception{ synchronized (Singleton.class) { if(singleton == null){ singleton = new Singleton(); } } return singleton; } }
6、單例模式之懶漢模式(線程安全)--雙重校驗鎖
分析:上面的例子我們可以看到,synchronized 其實將方法內部的所有語句都已經包括了,每一個進來的線程都要單獨進入同步代碼塊,判斷實例是否存在,這就造成了性能的浪費。那么我們可以想到,其實在第一次已經創建了實例的情況下,后面再獲取實例的時候,可不可以不進入這個同步代碼塊?
//懶漢模式線程安全--雙重鎖校驗 public class Singleton { //構造器私有化 private Singleton(){ } //在類的內部自己創建實例的引用 private static Singleton singleton = null; //提供get 方法以供外界獲取單例 public static Singleton getInstance() throws Exception{ if(singleton == null){ synchronized (Singleton.class) { if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
以上的真的完美解決了單例模式嗎?其實並沒有,請看下面:
7、單例模式之最終版
我們知道編譯就是將源代碼翻譯成機械碼的過程,而Java虛擬機的目標代碼不是本地機器碼,而是虛擬機代碼。編譯原理里面有個過程是編譯優化,就是指在不改變原來語義的情況下,通過調整語句的順序,來讓程序運行的更快,這個過程稱為 reorder。
JVM 只是一個標准,它並沒有規定有關編譯器優化的內容,也就是說,JVM可以自由的實現編譯器優化。
那么我們來再來考慮一下,創建一個變量需要哪些步驟?
①、申請一塊內存,調用構造方法進行初始化
②、分配一個指針指向該內存
而這兩步誰先誰后呢?也就是存在這樣一種情況:先開辟一塊內存,然后分配一個指針指向該內存,最后調用構造方法進行初始化。
那么針對單例模式的設計,就會存在這樣一個問題:線程 A 開始創建 Singleton 的實例,此時線程 B已經調用了 getInstance的()方法,首先判斷 instance 是否為 null。而我們上面說的那種模型, A 已經把 instance 指向了那塊內存,只是還沒來得及調用構造方法進行初始化,因此 B 檢測到 instance 不為 null,於是直接把 instance 返回了。那么問題出現了:盡管 instance 不為 null,但是 A 並沒有構造完成,就像一套房子已經給了你鑰匙,但是里面還沒有裝修,你並不能住進去。
解決方案:使用 volatile 關鍵字修飾 instance
我們知道在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。
volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。
//懶漢模式線程安全--volatile public class Singleton { //構造器私有化 private Singleton(){ } //在類的內部自己創建實例的引用 private static volatile Singleton singleton = null; //提供get 方法以供外界獲取單例 public static Singleton getInstance() throws Exception{ if(singleton == null){ synchronized (Singleton.class) { if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
到此我們完美的解決了單例模式的問題。但是 volatile 關鍵字是 JDK1.5 才有的,也就是 JDK1.5 之前是不能這樣用的
PS:我們還可以使用 枚舉類型 或靜態內部類來實現單例模式
8、單例模式之枚舉類
public enum Singleton{ INSTANCE; private Singleton(){} }
9、單例模式之靜態內部類
public class InnerSingleton { private InnerSingleton(){} public static InnerSingleton getInstance(){ return Inner.instance; } static class Inner{ static InnerSingleton instance = new InnerSingleton(); } public static void main(String [] args){ System.out.println(InnerSingleton.getInstance()==InnerSingleton.getInstance());//true System.out.println(InnerSingleton.getInstance().equals(InnerSingleton.getInstance()));//true }
單例模式的應用:
1、windows 系統的回收站,我們能在任何盤符刪除數據,但是最后都是到了回收站中
2、網站的計數器,不過不采用單例模式,很難實現同步
3、數據庫連接池,可以節省打開或關閉數據庫連接所引起的效率損耗,用單例模式來維護,可以大大降低這種損耗。
由上可以總結單例模式的應用場景:
①、資源共享
②、方便資源互相通信