作者:湯圓

個(gè)人博客:javalover.cc

前言

有時(shí)候我們的類并不需要很多個(gè)實(shí)例,在程序運(yùn)行期間,可能只需要一個(gè)實(shí)例就夠了,多了反而會(huì)出現(xiàn)數(shù)據(jù)不一致的問題;

這時(shí)候我們就可以用單例模式來實(shí)現(xiàn),然后程序中所有的操作都基于這個(gè)實(shí)例;

目錄

單例模式有很多種,這里我們先列舉下:

  • 餓漢模式
  • 懶漢模式-線程不安全
  • 懶漢模式-線程安全
  • 懶漢模式-線程不是很安全
  • 懶漢模式-雙重檢查
  • 靜態(tài)內(nèi)部類
  • 枚舉

正文

1. 餓漢模式(不推薦)

餓漢模式的核心就是第一次加載類的時(shí)候,進(jìn)行數(shù)據(jù)的初始化;

而且這個(gè)數(shù)據(jù)不可被修改(final);

后續(xù)只能讀,不能寫。

這樣一來,就保證了數(shù)據(jù)的準(zhǔn)確性;

下面我們看下示例

package pattern.singleton;// 餓漢模式(不推薦),因?yàn)檎純?nèi)存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);    }}

從主程序中可以看到,不管獲取多少次實(shí)例,都是同一個(gè)。

優(yōu)點(diǎn):在類第一次加載時(shí)就創(chuàng)建單例,線程安全

缺點(diǎn):占內(nèi)存,不管用不用,都要先加載

2. 懶漢模式-線程不安全(不推薦)

懶漢模式,就是類初始化時(shí)不加載數(shù)據(jù),等到需要的時(shí)候才加載;

下面看示例:

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);    }}

這樣做的好處就是節(jié)省資源,只在需要的時(shí)候才加載;

但是會(huì)導(dǎo)致一個(gè)問題,就是線程的安全性;

比如兩個(gè)線程同時(shí)獲取,有可能獲取到不同的實(shí)例;

優(yōu)點(diǎn):懶加載,使用時(shí)才加載,節(jié)省資源

缺點(diǎn):線程不安全,多線程有可能創(chuàng)建多個(gè)單例

3. 懶漢模式-線程安全(不推薦)

上面的懶漢模式,最大的缺點(diǎn)就是線程不安全;

所以我們可以升級(jí)一下,通過加鎖來解決,如下所示

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);    }}

這樣一來,不管多少個(gè)線程去獲取實(shí)例,都只會(huì)獲取到同一個(gè);

但是缺點(diǎn)也很明顯,就是效率低;

比如現(xiàn)在已經(jīng)遺棄的vector類,就是通過給方法上鎖,來解決安全問題

優(yōu)點(diǎn):線程安全,懶加載

缺點(diǎn):效率低,每次獲取單例都要操作鎖

4. 懶漢模式-線程不是很安全(不推薦)

這一次,我們又升級(jí)了上面的懶漢模式,把方法鎖改為代碼塊鎖,減小了鎖的范圍;

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. 所有的線程會(huì)先執(zhí)行下面的打印,然后第一個(gè)線程先獲得鎖,其他線程依次排隊(duì)等待解鎖            System.out.println(Thread.currentThread().getName());            synchronized (LazyDemo3.class){                try {                    System.out.println(Thread.currentThread().getName()+"等待中");                    // 2. 當(dāng)?shù)谝粋€(gè)進(jìn)來的線程在這里休眠時(shí),其他外面的線程是獲取不到鎖的,就會(huì)一直等待                    Thread.sleep(1000);                    lazyDemo = new LazyDemo3();                    System.out.println(Thread.currentThread().getName()+"等待結(jié)束");                    // 3. 此時(shí)第一個(gè)線程釋放鎖,第二個(gè)線程因?yàn)橐呀?jīng)通過了if(lazyDemo == null)的判斷                    // 所以會(huì)直接獲取鎖,然后重復(fù)剛才的步驟2,這樣就會(huì)導(dǎo)致實(shí)例 lazyDemo 被創(chuàng)建多次                } 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 // 此時(shí)Thread-1已經(jīng)通過了if()校驗(yàn) * Thread-0等待結(jié)束 // Thread-0 釋放鎖 * Thread-1等待中 // Thread-1 獲取鎖 * pattern.singleton.LazyDemo3@568acee4 // 這是 Thread-0 創(chuàng)建的單例 * Thread-1等待結(jié)束 // Thread-1 釋放鎖 * pattern.singleton.LazyDemo3@3f580216 // 這是 Thread-1 創(chuàng)建的單例,此時(shí)就有了兩個(gè)單例,就出問題了 * * */

通過例子可以看到,這兩個(gè)線程交替執(zhí)行去獲取實(shí)例,雖然效率有所提高,但是結(jié)果卻創(chuàng)建了兩個(gè)實(shí)例,因小失大

所以這種方式也不推薦

優(yōu)點(diǎn):懶加載,比上面的 LazyDemo2 效率高

缺點(diǎn):有可能導(dǎo)致線程不安全,詳情見代碼(需親測(cè)看到效果,才好理解)

5. 懶漢模式-雙重檢查(推薦)

前面的幾種懶漢模式,都是各有各的不足;

所以這里來個(gè)大招,將上面的不足都解決掉;

也就是雙重檢查模式。

package pattern.singleton;// 懶漢模式-雙重檢查(推薦)public class LazyDemo4 {    // 保證可見性,即在多線程時(shí),一個(gè)線程修改了這個(gè)變量,則其他線程立馬就可以看到變化    private static volatile LazyDemo4 lazyDemo;    private LazyDemo4(){    }    public static LazyDemo4 getInstance(){        if(lazyDemo == null)            // 加同步代碼塊,保證當(dāng)前只有一個(gè)線程在修改 lazyDemo            synchronized (LazyDemo4.class){                // 加雙重檢查,其他后面進(jìn)來的線程,如果看到 lazyDemo 已經(jīng)創(chuàng)建了,則不再創(chuàng)建,直接返回                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);    }}

可以看到,這里在獲取到鎖之后,又加了一個(gè)null判斷,這樣就可以保證在創(chuàng)建實(shí)例之前,確保實(shí)例真的是null

優(yōu)點(diǎn):懶加載、線程安全、效率高

6. 靜態(tài)內(nèi)部類(推薦)

這個(gè)就比較簡(jiǎn)單了,不需要加鎖,也不需要考慮null判斷,直接將實(shí)例封裝到內(nèi)部類中,再用final修飾為不可變;

從而保證了這個(gè)實(shí)例的唯一性;

這個(gè)其實(shí)就是結(jié)合了前面的 餓漢模式 和 懶漢模式-雙重檢查。

package pattern.singleton;// 靜態(tài)內(nèi)部類(推薦)public class StaticInnerDemo {    private StaticInnerDemo(){    };    // 靜態(tài)內(nèi)部類    // 1. 當(dāng) StaticInnerDemo 加載時(shí),下面的 InnerInstace 并沒有加載    // 2. 當(dāng) 調(diào)用getInstance()時(shí),下面的靜態(tài)內(nèi)部類才會(huì)加載,且只會(huì)加載一次(因?yàn)閒inal常量)    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);    }}

優(yōu)點(diǎn):懶加載,線程安全(詳情見代碼)

7. 枚舉(推薦)

最后來個(gè)壓軸的,通過枚舉來實(shí)現(xiàn)單例模式;

這個(gè)可以說是極簡(jiǎn)主義風(fēng)格,自帶單例效果;

因?yàn)椴恍枰^多的修飾,只是單純的定義一個(gè)枚舉,然后創(chuàng)建一個(gè)實(shí)例,后面程序直接用這個(gè)實(shí)例就可以了。

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);    }}

優(yōu)點(diǎn):懶加載,線程安全,效率高,大牛推薦(Effective Java作者推薦)

總結(jié)

關(guān)于單例模式的實(shí)現(xiàn)方式,首推的就是枚舉,其次是懶漢模式-雙重檢查,最后是靜態(tài)內(nèi)部類