作者:湯圓

個人博客: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作者推薦)

總結

關于單例模式的實現方式,首推的就是枚舉,其次是懶漢模式-雙重檢查,最后是靜態內部類