設(shè)計(jì)模式之單例模式(Singleton)
一、定義
一個類只有一個實(shí)例,它自己負(fù)責(zé)創(chuàng)建自己的對象,這個類提供了一種訪問其唯一對象的方式,可以直接訪問,不需要實(shí)例化該類的對象。
單例模式有以下三個特點(diǎn):
1、單例類只能有一個實(shí)例。
2、單例類必須自己創(chuàng)建自己的唯一實(shí)例。
3、單例類必須給所有其他對象提供這一實(shí)例。
如果要實(shí)現(xiàn)這三點(diǎn),需要滿足如下三個要求:
1、單例類中含有一個該類的私有的靜態(tài)實(shí)例。
2、單例類只提供私有的構(gòu)造函數(shù)。
3、單例類提供一個公有的靜態(tài)的函數(shù)用于獲取它本身的私有靜態(tài)實(shí)例。

二、單例模式示例
1、單例模式分類
單例模式根據(jù)實(shí)例的創(chuàng)建時機(jī)來劃分可分為:餓漢式和懶漢式。
餓漢式:應(yīng)用剛啟動的時候,不管外部有沒有調(diào)用該類的實(shí)例方法,該類的實(shí)例就已經(jīng)創(chuàng)建好了。
懶漢式:應(yīng)用剛啟動的時候,并不創(chuàng)建實(shí)例,等到第一次被使用時,才創(chuàng)建該類的實(shí)例。
2、舉例說明
(1)、餓漢式(線程安全,可用)
1 public class Singleton {
2 // 私有的靜態(tài)實(shí)例
3 private final static Singleton instance = new Singleton();
4
5 // 私有的構(gòu)造函數(shù),防止在該類外部通過new創(chuàng)建實(shí)例
6 private Singleton(){
7 System.out.println("創(chuàng)建Singleton實(shí)例!");
8 }
9
10 // 公有的靜態(tài)的函數(shù),用于獲取實(shí)例
11 public static Singleton getInstance(){
12 return instance;
13 }
14}
15
16public class Test {
17 public static void main(String[] args) {
18 Singleton instance1 = Singleton.getInstance();
19 System.out.println(instance1);
20
21 Singleton instance2 = Singleton.getInstance();
22 System.out.println(instance2);
23
24 Singleton instance3 = Singleton.getInstance();
25 System.out.println(instance3);
26 }
27}
程序運(yùn)行結(jié)果:
創(chuàng)建Singleton實(shí)例!
com.zxj.test.Singleton@154617c
com.zxj.test.Singleton@154617c
com.zxj.test.Singleton@154617c
觀察程序運(yùn)行結(jié)果:只創(chuàng)建了一個實(shí)例,三次調(diào)用獲取到的實(shí)例都是同一個。
餓漢式優(yōu)點(diǎn):
在類加載的時候就完成了實(shí)例化,避免了多線程的同步問題。
餓漢式缺點(diǎn):
在外部沒有使用到該類的時候,該類的實(shí)例就創(chuàng)建了,若該類的實(shí)例的創(chuàng)建比較消耗系統(tǒng)資源,并且外部一直沒有調(diào)用該實(shí)例,那么這部分的系統(tǒng)資源的消耗是沒有意義的。
(2)、懶漢式(線程不安全,不可用)
(a).單線程環(huán)境下,是沒有問題的:
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("創(chuàng)建Singleton實(shí)例!");
6 }
7
8 public static Singleton getInstance(){
9 if(instance == null){
10 instance = new Singleton();
11 }
12 return instance;
13 }
14}
15
16public class Test {
17 public static void main(String[] args) {
18 Singleton instance1 = Singleton.getInstance();
19 System.out.println(instance1);
20
21 Singleton instance2 = Singleton.getInstance();
22 System.out.println(instance2);
23
24 Singleton instance3 = Singleton.getInstance();
25 System.out.println(instance3);
26 }
27}
程序運(yùn)行結(jié)果:
創(chuàng)建Singleton實(shí)例!
com.zxj.test2.Singleton@154617c
com.zxj.test2.Singleton@154617c
com.zxj.test2.Singleton@154617c
(b).多線程環(huán)境下,單例模式失效,程序中有多個實(shí)例:
1public class Test {
2 public static void main(String[] args) {
3 for(int i = 0; i < 10; i++){
4 new Thread(){
5 @Override
6 public void run(){
7 Singleton instance = Singleton.getInstance();
8 System.out.println(instance);
9 }
10 }.start();
11 }
12 }
13}
程序運(yùn)行結(jié)果:
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
創(chuàng)建Singleton實(shí)例!
com.zxj.test2.Singleton@1329a4
com.zxj.test2.Singleton@10ada9c
com.zxj.test2.Singleton@16268e5
com.zxj.test2.Singleton@95e4d4
com.zxj.test2.Singleton@2191b2
com.zxj.test2.Singleton@173a9e8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1122c7b
com.zxj.test2.Singleton@16ab049
com.zxj.test2.Singleton@15cc318
我們來分析一下,為什么在多線程環(huán)境下,懶漢式單例模式會失效?
假設(shè)有兩個線程,線程A和線程B,線程A執(zhí)行到第10行,但是并沒有執(zhí)行這一行,這個時候,線程B執(zhí)行到第9行,線程B判斷結(jié)果是true,線程B執(zhí)行第10行,new了一個對象,此時,線程A,這就造成執(zhí)行個對象被new了兩次。
假設(shè)有兩個線程,線程A和線程B,線程A先得到CPU的執(zhí)行權(quán),線程A執(zhí)行到第9行 ,由于instance之前并沒有實(shí)例化,所以線程A判斷結(jié)果為true,線程A還沒有來得及執(zhí)行第10行,CPU執(zhí)行權(quán)就被線程B搶去了,線程B執(zhí)行到第9行,由于instance之前并沒有實(shí)例化,所以線程B判斷結(jié)果為true,此時線程B new了一個實(shí)例。之后CPU執(zhí)行權(quán)分給線程A,線程A接著執(zhí)行第10行實(shí)例的創(chuàng)建。由此看到線程A和線程B分別創(chuàng)建了一個實(shí)例(存在2個實(shí)例了),這就導(dǎo)致了單例的失效。
那么如何保證懶漢式在多線程環(huán)境仍然保證只有一個單例呢?當(dāng)然是在創(chuàng)建實(shí)例時進(jìn)行同步控制。
(3)、懶漢式(線程安全,可用)
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("創(chuàng)建Singleton實(shí)例!");
6 }
7
8 // 對整個獲取實(shí)例的方法進(jìn)行同步
9 public static synchronized Singleton getInstance(){
10 if(instance == null){
11 instance = new Singleton();
12 }
13 return instance;
14 }
15}
這種寫法,對獲取實(shí)例的整個方法用synchronized關(guān)鍵字進(jìn)行方法同步,保證了同一時刻只能有一個線程能夠訪問并獲得實(shí)例。
但是缺點(diǎn)也很明顯,這里鎖住的是整個方法,鎖的粒度太大,造成效率低下,那應(yīng)該怎么辦呢?減小鎖的粒度,只把創(chuàng)建實(shí)例這塊代碼上鎖,見下面的雙重校驗(yàn)鎖方式。
(4)、雙重校驗(yàn)鎖(DCL,double-checked locking,線程安全,可用)
雙重校驗(yàn)鎖,雙重校驗(yàn)說的是兩個空值判斷,鎖說的是synchronized。
(a).雙重校驗(yàn)鎖代碼
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("創(chuàng)建Singleton實(shí)例!");
6 }
7 // 對必要的代碼塊(創(chuàng)建實(shí)例的代碼塊)進(jìn)行同步
8 public static Singleton getInstance(){
9 if (instance == null) {
10 synchronized (Singleton.class) {
11 if (instance == null) {
12 instance = new Singleton();
13 }
14 }
15 }
16 return instance;
17 }
18}
1public class Test {
2 public static void main(String[] args) {
3 for(int i = 0; i < 10; i++){
4 new Thread(){
5 @Override
6 public void run(){
7 Singleton instance = Singleton.getInstance();
8 System.out.println(instance);
9 }
10 }.start();
11 }
12 }
13}
程序運(yùn)行結(jié)果:
創(chuàng)建Singleton實(shí)例!
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
(b).關(guān)于雙重校驗(yàn)鎖的兩個疑問
從程序運(yùn)行結(jié)果可以看到,程序中只有一個實(shí)例。關(guān)于雙重校驗(yàn)鎖,小伙伴們一定會有一些疑問,下面我們就來共同一一解答。
問題1:既然有了一次空值判斷,為什么還要再加一層空值判斷呢?
答案:這主要涉及線程安全的問題。
這個問題的意思,就是在第9行進(jìn)行實(shí)例非空判斷之后,進(jìn)入synchronized代碼塊之后就不必再進(jìn)行一次非空判斷了,我們來看一下,如果這樣做的話,會產(chǎn)生什么問題?
假設(shè)有兩個線程,線程A和線程B,線程A先得到CPU的執(zhí)行權(quán),在執(zhí)行到第9行時,由于之前沒有實(shí)例化,所以線程A判斷結(jié)果為true,然后線程A獲得鎖進(jìn)入synchronized代碼塊里面。
此時線程B爭搶到CPU的執(zhí)行權(quán),并執(zhí)行到第9行,此時線程A還沒有執(zhí)行實(shí)例化動作,所以此時線程B判斷為true,線程B想進(jìn)入同步代碼塊,但是發(fā)現(xiàn)鎖還在線程A手里,所以B只能在同步代碼塊外面等待。
此時線程A得回CPU執(zhí)行權(quán),執(zhí)行實(shí)例化動作并返回該實(shí)例,然后釋放鎖。
線程A釋放了鎖,線程B就獲得了鎖,若此時不進(jìn)行第二次非空判斷,會導(dǎo)致線程B也實(shí)例化創(chuàng)建一個實(shí)例,然后返回自己創(chuàng)建的實(shí)例,這就導(dǎo)致了2個線程訪問創(chuàng)建了2個實(shí)例,導(dǎo)致單例失效。
若進(jìn)行第二次非空判斷,線程B發(fā)現(xiàn)線程A已經(jīng)創(chuàng)建了實(shí)例,則直接返回線程A創(chuàng)建的實(shí)例,這樣就避免了單例的失效。
問題2:即便將第9行的空值判斷去掉,在多線程環(huán)境下單例模式仍然有效,那為什么多次一舉加上第9行代碼呢?
答案:這主要涉及多線程下的效率問題。
使用synchronized關(guān)鍵字進(jìn)行同步,意味著同一時刻只能有一個線程執(zhí)行同步塊里面的代碼,還要涉及到鎖的爭奪、釋放等問題,是很消耗資源的。如果我們不加第9行,即不在進(jìn)入同步塊之前進(jìn)行非空判斷,如果之前已經(jīng)有線程創(chuàng)建了該類的實(shí)例了,那每次訪問獲取實(shí)例的方法都會進(jìn)入同步塊,這會非常的耗費(fèi)性能。如果在進(jìn)入同步塊之前加上非空判斷,如果之前已經(jīng)有線程創(chuàng)建了該類的實(shí)例了,那就不必進(jìn)入同步塊了,直接返回之前創(chuàng)建的實(shí)例即可,減少synchronized操作次數(shù),從而提高 程序性能。
(c).DCL失效問題,以及解決方案
在第12行,instance = new Singleton();,其實(shí)這行代碼在JVM里面的執(zhí)行分三步:
1.在堆內(nèi)存中分配內(nèi)存空間給這個對象。
2.初始化這個對象。
3.設(shè)置instance指向剛分配的內(nèi)存空間。
由于jvm的指令重排序功能,可能在2還沒執(zhí)行時就先執(zhí)行了3,此時第12行就變成:
1.在堆內(nèi)存中分配內(nèi)存空間給這個對象。
2.設(shè)置instance指向剛分配的內(nèi)存空間。
3.初始化這個對象。
假設(shè)有兩個線程,線程A和線程B,并且第12行代碼發(fā)生指令重排序。
線程A先獲取到CPU執(zhí)行權(quán),并執(zhí)行到第9行。
此時線程B爭搶到CPU執(zhí)行權(quán),并執(zhí)行完第12行的第2個步驟。
此時線程A奪回CPU執(zhí)行權(quán),由于線程B執(zhí)行第12行的第2個步驟,instance已經(jīng)非空了,它會被線程A直接拿來用,這樣的話,就會出現(xiàn)異常,因?yàn)閕nstance只是一個引用,它指向的對象還沒有實(shí)例化呢,自然會出現(xiàn)問題了。這個就是著名的DCL失效問題。
解決DCL失效問題,有兩種方案,一是使用volatile關(guān)鍵字禁止指令重排序,二是使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式。
使用volatile關(guān)鍵字禁止指令重排序:
1public class Singleton {
2 private volatile static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("創(chuàng)建Singleton實(shí)例!");
6 }
7 // 對必要的代碼塊(創(chuàng)建實(shí)例的代碼塊)進(jìn)行同步
8 public static Singleton getInstance(){
9 if (instance == null) {
10 synchronized (Singleton.class) {
11 if (instance == null) {
12 instance = new Singleton();
13 }
14 }
15 }
16 return instance;
17 }
18}
(5)、靜態(tài)內(nèi)部類 (可用,推薦)
1public class Singleton {
2 private Singleton() {}
3
4 private static class SingletonInstance {
5 private static final Singleton instance = new Singleton();
6 }
7 // 靜態(tài)內(nèi)部類
8 public static Singleton getInstance() {
9 return SingletonInstance.instance;
10 }
11
12}
使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式,是在實(shí)際開發(fā)中最推薦的寫法。
問題1:靜態(tài)內(nèi)部類,如何保證延遲加載?
當(dāng)Singleton類第一次被加載時并不會立即實(shí)例化,而是當(dāng)getInstance()方法第一次被調(diào)用時,才會去加載SingletonInstance類,從而完成Singleton的實(shí)例化。
問題2:靜態(tài)內(nèi)部類,如何保證程序中只有一個實(shí)例?
因?yàn)轭惖撵o態(tài)屬性只會在第一次加載類的時候初始化,也就保證了SingletonInstance中的對象只會被實(shí)例化一次。
問題3:如何保證靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例模式在多線程環(huán)境是生效的?
有兩點(diǎn)原因,一是虛擬機(jī)規(guī)范規(guī)定,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的
綜合這兩點(diǎn)以及第5行代碼,在多線程環(huán)境,只會有一個線程創(chuàng)建一個實(shí)例,其他線程用的都是這個線程創(chuàng)建的這個實(shí)例。
(6)、枚舉 (可用、推薦)
public enum Singleton {
INSTANCE;
}
這種方式是《Effective JAVA》作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機(jī)制,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實(shí)例化。
單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法
-- 出自 《effective java》
(7)、在實(shí)際開發(fā)中,選擇哪種單例模式的實(shí)現(xiàn)方式?
一般情況下,不建議使用第2種和第3種懶漢方式,建議使用第1種餓漢方式。只有在要明確實(shí)現(xiàn) lazy loading 效果時,才會使用第5種靜態(tài)內(nèi)部類方式。如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用第6種枚舉方式。如果有其他特殊的需求,可以考慮使用第4種雙重校驗(yàn)鎖方式。
三、單例模式優(yōu)點(diǎn)
(1)在內(nèi)存里只有一個實(shí)例,減少了內(nèi)存開銷,特別是一個對象需要頻繁創(chuàng)建和銷毀,而且創(chuàng)建和銷毀時的性能又無法優(yōu)化時,這個時候,把它設(shè)置成單例可以提高系統(tǒng)性能。
(2)可以避免對資源的多重占用,比如對一個文件進(jìn)行寫操作,由于只有一個連接實(shí)例存在內(nèi)存中,可以避免對一個資源文件同時進(jìn)行寫操作。
(3)設(shè)置全局訪問點(diǎn),嚴(yán)格控制訪問。例如Web應(yīng)用的頁面計(jì)數(shù)器就可以用單例模式來實(shí)現(xiàn),從而保證計(jì)數(shù)的準(zhǔn)確性。
四、單例模式缺點(diǎn)
(1)不適用于變化頻繁的對象,如果同一類型的對象總是要在不同的用例場景發(fā)生變化,單例模式就會引起數(shù)據(jù)的錯誤,不能保存彼此的狀態(tài)。
(2)由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。
(3)單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。
(3)如果實(shí)例化的對象長時間不被利用,系統(tǒng)會認(rèn)為該對象是垃圾而被回收,可能會導(dǎo)致對象狀態(tài)的丟失。
五、單例模式的使用場景
(1)要求一個類在程序的生命周期當(dāng)中只有一個實(shí)例。比如在Spring框架項(xiàng)目中,若某個類只需要有一個實(shí)例,那么只要把Spring配置文件該類的
(2)需要頻繁實(shí)例化然后銷毀的對象,也就是頻繁的 new 對象,可以考慮用單例模式替換。
(3)創(chuàng)建對象時耗時過多或者耗資源過多,但又經(jīng)常用到的對象,比如數(shù)據(jù)庫連接。
下面我們舉個例子,講解單例模式的使用:網(wǎng)站在線人數(shù)統(tǒng)計(jì)
其實(shí)就是一個全局計(jì)數(shù)器,也就是說所有用戶在相同的時刻獲取到的在線人數(shù)數(shù)量都是一致的。要實(shí)現(xiàn)這個需求,計(jì)數(shù)器就要全局唯一,也就正好可以用單例模式來實(shí)現(xiàn)(利用單例模式的第三個優(yōu)點(diǎn):全局訪問)。
public class Counter {
// 使用靜態(tài)類實(shí)現(xiàn)單例模式
private static class CounterHolder{
private static final Counter counter = new Counter();
}
// 私有構(gòu)造器
private Counter(){
System.out.println("init...");
}
// 獲取實(shí)例的方法
public static final Counter getInstance(){
return CounterHolder.counter;
}
// 使用AtomicLong類型存儲計(jì)數(shù)
private AtomicLong online = new AtomicLong();
// 獲取計(jì)數(shù)
public long getOnline(){
return online.get();
}
// 計(jì)數(shù)加1
public long add(){
return online.incrementAndGet();
}
}
