作者:湯圓
個人博客:javalover.cc
前言
有時候我們的類并不需要很多個實例,在程序運行期間,可能只需要一個實例就夠了,多了反而會出現數據不一致的問題;
這時候我們就可以用單例模式來實現,然后程序中所有的操作都基于這個實例;
目錄
單例模式有很多種,這里我們先列舉下:
- 餓漢模式
- 懶漢模式-線程不安全
- 懶漢模式-線程安全
- 懶漢模式-線程不是很安全
- 懶漢模式-雙重檢查
- 靜態內部類
- 枚舉
正文
1. 餓漢模式(不推薦)
餓漢模式的核心就是第一次加載類的時候,進行數據的初始化;
而且這個數據不可被修改(final);
后續只能讀,不能寫。
這樣一來,就保證了數據的準確性;
下面我們看下示例
package pattern.singleton;// 餓漢模式(不推薦),因為占內存public class HungryDemo { private static final HungryDemo hungryDemo = new HungryDemo(); private HungryDemo() { } public static HungryDemo getInstance(){ return hungryDemo; } public static void main(String[] args) { HungryDemo hungryDemo1 = HungryDemo.getInstance(); HungryDemo hungryDemo2 = HungryDemo.getInstance(); System.out.println(hungryDemo1); System.out.println(hungryDemo2); System.out.println(hungryDemo1 == hungryDemo2); }}
從主程序中可以看到,不管獲取多少次實例,都是同一個。
優點:在類第一次加載時就創建單例,線程安全
缺點:占內存,不管用不用,都要先加載
2. 懶漢模式-線程不安全(不推薦)
懶漢模式,就是類初始化時不加載數據,等到需要的時候才加載;
下面看示例:
package pattern.singleton;// 懶漢模式-線程不安全(不推薦)public class LazyDemo1 { private static LazyDemo1 lazyDemo; private LazyDemo1(){ } public static LazyDemo1 getInstance(){ if(lazyDemo == null) lazyDemo = new LazyDemo1(); return lazyDemo; } public static void main(String[] args) { LazyDemo1 l1 = LazyDemo1.getInstance(); LazyDemo1 l2 = LazyDemo1.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
這樣做的好處就是節省資源,只在需要的時候才加載;
但是會導致一個問題,就是線程的安全性;
比如兩個線程同時獲取,有可能獲取到不同的實例;
優點:懶加載,使用時才加載,節省資源
缺點:線程不安全,多線程有可能創建多個單例
3. 懶漢模式-線程安全(不推薦)
上面的懶漢模式,最大的缺點就是線程不安全;
所以我們可以升級一下,通過加鎖來解決,如下所示
package pattern.singleton;// 懶漢模式-線程安全(不推薦)public class LazyDemo2 { private static LazyDemo2 lazyDemo; private LazyDemo2(){ } // 給方法加鎖,線程安全了,但是效率低 public static synchronized LazyDemo2 getInstance(){ if(lazyDemo == null) lazyDemo = new LazyDemo2(); return lazyDemo; } public static void main(String[] args) { LazyDemo2 l1 = LazyDemo2.getInstance(); LazyDemo2 l2 = LazyDemo2.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
這樣一來,不管多少個線程去獲取實例,都只會獲取到同一個;
但是缺點也很明顯,就是效率低;
比如現在已經遺棄的vector類,就是通過給方法上鎖,來解決安全問題
優點:線程安全,懶加載
缺點:效率低,每次獲取單例都要操作鎖
4. 懶漢模式-線程不是很安全(不推薦)
這一次,我們又升級了上面的懶漢模式,把方法鎖改為代碼塊鎖,減小了鎖的范圍;
package pattern.singleton;import java.lang.management.ThreadInfo;// 懶漢模式-線程不安全(不推薦)// 解釋:雖然加了代碼同步塊,但是還是存在線程不安全的情況public class LazyDemo3 { private static LazyDemo3 lazyDemo; private static int count = 0; private LazyDemo3(){ } public static LazyDemo3 getInstance(){ if(lazyDemo == null){ // 1. 所有的線程會先執行下面的打印,然后第一個線程先獲得鎖,其他線程依次排隊等待解鎖 System.out.println(Thread.currentThread().getName()); synchronized (LazyDemo3.class){ try { System.out.println(Thread.currentThread().getName()+"等待中"); // 2. 當第一個進來的線程在這里休眠時,其他外面的線程是獲取不到鎖的,就會一直等待 Thread.sleep(1000); lazyDemo = new LazyDemo3(); System.out.println(Thread.currentThread().getName()+"等待結束"); // 3. 此時第一個線程釋放鎖,第二個線程因為已經通過了if(lazyDemo == null)的判斷 // 所以會直接獲取鎖,然后重復剛才的步驟2,這樣就會導致實例 lazyDemo 被創建多次 } catch (InterruptedException e) { e.printStackTrace(); } } } return lazyDemo; } public static void main(String[] args) { for (int i = 0; i < 2; i++) { new Thread(new Runnable() { @Override public void run() { LazyDemo3 l1 = LazyDemo3.getInstance(); System.out.println(l1); } }).start(); } }}/** * 下面是輸出 * * Thread-0 * Thread-0等待中 * Thread-1 // 此時Thread-1已經通過了if()校驗 * Thread-0等待結束 // Thread-0 釋放鎖 * Thread-1等待中 // Thread-1 獲取鎖 * pattern.singleton.LazyDemo3@568acee4 // 這是 Thread-0 創建的單例 * Thread-1等待結束 // Thread-1 釋放鎖 * pattern.singleton.LazyDemo3@3f580216 // 這是 Thread-1 創建的單例,此時就有了兩個單例,就出問題了 * * */
通過例子可以看到,這兩個線程交替執行去獲取實例,雖然效率有所提高,但是結果卻創建了兩個實例,因小失大
所以這種方式也不推薦
優點:懶加載,比上面的 LazyDemo2 效率高
缺點:有可能導致線程不安全,詳情見代碼(需親測看到效果,才好理解)
5. 懶漢模式-雙重檢查(推薦)
前面的幾種懶漢模式,都是各有各的不足;
所以這里來個大招,將上面的不足都解決掉;
也就是雙重檢查模式。
package pattern.singleton;// 懶漢模式-雙重檢查(推薦)public class LazyDemo4 { // 保證可見性,即在多線程時,一個線程修改了這個變量,則其他線程立馬就可以看到變化 private static volatile LazyDemo4 lazyDemo; private LazyDemo4(){ } public static LazyDemo4 getInstance(){ if(lazyDemo == null) // 加同步代碼塊,保證當前只有一個線程在修改 lazyDemo synchronized (LazyDemo4.class){ // 加雙重檢查,其他后面進來的線程,如果看到 lazyDemo 已經創建了,則不再創建,直接返回 if(lazyDemo == null) lazyDemo = new LazyDemo4(); } return lazyDemo; } public static void main(String[] args) { LazyDemo4 l1 = LazyDemo4.getInstance(); LazyDemo4 l2 = LazyDemo4.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
可以看到,這里在獲取到鎖之后,又加了一個null判斷,這樣就可以保證在創建實例之前,確保實例真的是null
優點:懶加載、線程安全、效率高
6. 靜態內部類(推薦)
這個就比較簡單了,不需要加鎖,也不需要考慮null判斷,直接將實例封裝到內部類中,再用final修飾為不可變;
從而保證了這個實例的唯一性;
這個其實就是結合了前面的 餓漢模式 和 懶漢模式-雙重檢查。
package pattern.singleton;// 靜態內部類(推薦)public class StaticInnerDemo { private StaticInnerDemo(){ }; // 靜態內部類 // 1. 當 StaticInnerDemo 加載時,下面的 InnerInstace 并沒有加載 // 2. 當 調用getInstance()時,下面的靜態內部類才會加載,且只會加載一次(因為final常量) private static class InnerInstance{ private static final StaticInnerDemo staticInnerDemo = new StaticInnerDemo(); } public static StaticInnerDemo getInstance(){ return InnerInstance.staticInnerDemo; } public static void main(String[] args) { StaticInnerDemo staticInnerDemo1 = getInstance(); StaticInnerDemo staticInnerDemo2 = getInstance(); System.out.println(staticInnerDemo1); System.out.println(staticInnerDemo2); System.out.println(staticInnerDemo1 == staticInnerDemo2); }}
優點:懶加載,線程安全(詳情見代碼)
7. 枚舉(推薦)
最后來個壓軸的,通過枚舉來實現單例模式;
這個可以說是極簡主義風格,自帶單例效果;
因為不需要過多的修飾,只是單純的定義一個枚舉,然后創建一個實例,后面程序直接用這個實例就可以了。
package pattern.singleton;// 枚舉(推薦)public enum EnumDemo { INSTANCE; public static void main(String[] args) { EnumDemo instance1 = EnumDemo.INSTANCE; EnumDemo instance2 = EnumDemo.INSTANCE; System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); }}
優點:懶加載,線程安全,效率高,大牛推薦(Effective Java作者推薦)
總結
關于單例模式的實現方式,首推的就是枚舉,其次是懶漢模式-雙重檢查,最后是靜態內部類