一,前言html
單例模式詳細你們都已經很是熟悉了,在文章單例模式的八種寫法比較中,對單例模式的概念以及使用場景都作了很不錯的說明。請在閱讀本文以前,閱讀一下這篇文章,由於本文就是按照這篇文章中的八種單例模式進行探索的。java
本文的目的是:結合文章中的八種單例模式的寫法,使用實際的示例,來演示線程安全和效率設計模式
既然是實際的示例,那麼就首先定義一個業務場景:購票。你們都知道在春運的時候,搶票是很是激烈的。有可能同一張票就同時又成百上千的人同時在搶。這就對代碼邏輯的要求很高了,即不能把同一張票屢次出售,也不能出現票號相同的票。
安全
那麼,接下來咱們就使用單例模式,實現票號的生成。同時呢在這個過程當中利用上述文章中的八種單例模式的寫法,來實踐這八種單例模式的線程安全性和比較八種單例模式的效率。多線程
既然文章中第三種單例模式(懶漢式)是線程不安全的,那麼我就從這個單例模式的實現開始探索一下線程安全。源碼分析
由於不論是八種單例模式的實現方式的哪種,票號的生成邏輯都是同樣的,因此,在此正式開始以前,爲了更方便的編寫示例代碼,先作一些準備工做:封裝票號生成父類代碼。post
二,封裝票號生成父類代碼測試
package com.zcz.singleton; public class TicketNumberHandler { //記錄下一個惟一的號碼 private long nextUniqueNumber = 1; /** * 返回生成的號碼 * @return */ public Long getTicketNumber() { return nextUniqueNumber++; } }
票號的生成邏輯很簡單,就是一個遞增的整數,每獲取一次,就增長1。之後咱們的每一種單例模式都繼承這個父類,就不用每一次都編寫這部分代碼,作到了代碼的重用。優化
接下來就是實現第三種單例模式,探索一下會不會引發線程安全問題。atom
三,實現第三種單例模式
package com.zcz.singleton; /** * 票號生成類——單利模式,即整個系統中只有惟一的一個實例 * @author zhangchengzi * */ public class TicketNumberHandler3 extends TicketNumberHandler{ //保存單例實例對象 private static TicketNumberHandler3 INSTANCE; //私有化構造方法 private TicketNumberHandler3() {}; /** * 懶漢式,在第一次獲取單例對象的時候初始化對象 * @return */ public static TicketNumberHandler3 getInsatance() { if(INSTANCE == null) { try { //這裏爲何要讓當前線程睡眠1毫秒呢? //由於在正常的業務邏輯中,單利模式的類不可能這麼簡單,因此實例化時間會多一些 //讓當前線程睡眠1毫秒 Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new TicketNumberHandler3(); } return INSTANCE; } }
代碼與上述文章的如出一轍,那麼接下來就開始編寫測試代碼。
四,編寫測試代碼
package com.zcz.singleton; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.Vector; public class BuyTicket { public static void main(String[] args) { // 用戶人數 int userNumber = 10000; // 保存用戶線程 Set<Thread> threadSet = new HashSet(); // 用於存放TicketNumberHandler實例對象 List<TicketNumberHandler> hanlderList = new Vector(); // 保存生成的票號 List<Long> ticketNumberList = new Vector(); // 定義購票線程,一個線程模擬一個用戶 for(int i=0;i<userNumber;i++) { Thread t = new Thread() { public void run() { TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; }; threadSet.add(t); } System.out.println("當前購票人數:"+threadSet.size()+" 人"); //記錄購票開始時間 long beginTime = System.currentTimeMillis(); for(Thread t : threadSet) { //開始購票 t.start(); } //記錄購票結束時間 long entTime; while(true) { //除去mian線程以外的全部線程結果後在記錄結束時間 if(Thread.activeCount() == 1) { entTime = System.currentTimeMillis(); break; } } //開始統計 System.out.println("票號生成類實例對象數目:"+new HashSet(hanlderList).size()); System.out.println("共出票:"+ticketNumberList.size()+"張"); System.out.println("實際出票:"+new HashSet(ticketNumberList).size()+"張"); System.out.println("出票用時:"+(entTime - beginTime)+" 毫秒"); } }
結合着代碼中的註釋,相信這部分測試代碼理解起來並不難,首先初始化10000個線程,至關於10000個用戶同時購票,而後啓動這10000個線程開始購票,結束後作統計。
這裏對代碼中的hanlderList和ticketNumberList進行一下說明:
1,這連個List的做用是什麼?這兩個List是用來作統計的。
hanlderList用來存放單例對象,而後在最後統計的部分會轉換爲Set,去除重複的對象,剩餘的對象數量就是真正的單例對象數量。若是真的是可是模式的話,在最後的統計打印的時候,票號生成類實例對象數目,應該是1。
ticketNumberList是用來存放票號的,一樣的在最後的統計部分也會轉換爲Set去重,若是真的有存在重複的票號,那麼打印信息中的實際出票數量應該小於共出票數量
2,這兩個List爲何使用Vector而不是ArrayList,由於ArrayList是線程不安全的,若是使用ArrayList,在最後的統計中ArrayList 會出現null,這樣咱們的數據就不許確了。
那麼,開始測試。
五,第三中單例模式的測試結果
右鍵 -> Run As -> Java Application。打印結果:
當前購票人數:10000 人 票號生成類實例對象數目:19 共出票:10000張 實際出票:9751張 出票用時:1130 毫秒
能夠看到:
票號生成類實例對象數目:19
說明不僅是有一個單例對象產生,緣由在上述的文章中也作了解釋說明。同時「共出票「實際出票數量」小於「共出票」屬性,說明產生了票號相同的票。
ok,線程不安全的第三種單例示例結果以後,還有7中可用的線程安全的實現方式,咱們就從1-8的順序逐一檢測,並經過執行時間來檢測效率高低。
六,測試第一種單例模式:使用靜態屬性,並初始化單例
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler1 extends TicketNumberHandler{ // 餓漢式,在類加載的時候初始化對象 private static TicketNumberHandler1 INSTANCE = new TicketNumberHandler1(); //私有化構造方法 private TicketNumberHandler1() {}; /** * 獲取單例實例 * @return */ public static TicketNumberHandler1 getInstance() { return INSTANCE; } }
2,修改測試類中使用的單例
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); TicketNumberHandler handler = TicketNumberHandler1.getInstance(); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1093 毫秒
跟上一次的打印結果相比對,票號生成類實例對象數目確實只有一個了,這說明第一種單例模式,在多線程下是能夠正確使用的。
並且,實際出票數量和共出票數量相同,也是沒有出現重複的票號的。可是真的是這樣的嗎?我麼把用戶數量調整到20000人,多執行幾回代碼試試看,你會發現偶爾會出現下面的打印結果:
當前購票人數:20000 人 票號生成類實例對象數目:1 共出票:20000張 實際出票:19996張 出票用時:5291 毫秒
票號生成類的實例對象一直是1,這沒問題,由於單例模式在多線程環境下正確執行了。
可是實際出票數量小於了共出票數量,這說明出現了重複的票號,爲何呢?由於咱們票號的生成方法,不是線程安全的
public Long getTicketNumber() { return nextUniqueNumber++; }
代碼中的nextUniqueNumber++是不具有原子性的,雖然看起來只有一行代碼,可是實際上執行了三個步驟:讀取nextUniqueNumber的值,將nextUniqueNumber的值加一,將結果賦值給nextUniqueNumber。
因此出現重複票號的緣由在於:在賦值沒有結束前,有多個線程讀取了值。
怎麼優化呢?最簡單的就是使用同步鎖。在getTicketNumber上添加關鍵字synchronized。
public synchronized Long getTicketNumber() { return nextUniqueNumber++; }
還有另一個方法,就是使用線程安全的AtomicLong
package com.zcz.singleton; import java.util.concurrent.atomic.AtomicLong; public class TicketNumberHandler { private AtomicLong nextUniqueNumber = new AtomicLong(); //記錄下一個惟一的號碼 // private long nextUniqueNumber = 1; /** * 返回生成的號碼 * @return */ public synchronized Long getTicketNumber() { // return nextUniqueNumber++; return nextUniqueNumber.incrementAndGet(); } }
ok,解決了這裏的問題以後,咱們將用戶人數,從新調整到10000人,運行10次,統計平均執行時間:1154.3毫秒
七,測試第二種單例模式:使用靜態代碼塊
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler2 extends TicketNumberHandler { // 餓漢式 private static TicketNumberHandler2 INSTANCE; //使用靜態代碼塊,初始化對象 static { INSTANCE = new TicketNumberHandler2(); } //私有化構造方法 private TicketNumberHandler2() {}; /** * 獲取單例實例 * @return */ public static TicketNumberHandler2 getInstance() { return INSTANCE; } }
2,修改測試代碼
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); TicketNumberHandler handler = TicketNumberHandler2.getInstance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1234 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1237.1毫秒
八,測試第四種單例模式:使用方法同步鎖(synchronized)
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler4 extends TicketNumberHandler { //保存單例實例對象 private static TicketNumberHandler4 INSTANCE; //私有化構造方法 private TicketNumberHandler4() {}; /** * 懶漢式,在第一次獲取單例對象的時候初始化對象 * @return */ public synchronized static TicketNumberHandler4 getInsatance() { if(INSTANCE == null) { try { //這裏爲何要讓當前線程睡眠1毫秒呢? //由於在正常的業務邏輯中,單利模式的類不可能這麼簡單,因此實例化時間會多一些 //讓當前線程睡眠1毫秒 Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new TicketNumberHandler4(); } return INSTANCE; } }
2,修改測試代碼
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); // TicketNumberHandler handler = TicketNumberHandler2.getInstance(); TicketNumberHandler handler = TicketNumberHandler4.getInsatance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1079 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1091.86毫秒
九,測試第五種單例模式:使用同步代碼塊
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler5 extends TicketNumberHandler { //保存單例實例對象 private static TicketNumberHandler5 INSTANCE; //私有化構造方法 private TicketNumberHandler5() {}; /** * 懶漢式,在第一次獲取單例對象的時候初始化對象 * @return */ public static TicketNumberHandler5 getInsatance() { if(INSTANCE == null) { synchronized (TicketNumberHandler5.class) { try { //這裏爲何要讓當前線程睡眠1毫秒呢? //由於在正常的業務邏輯中,單利模式的類不可能這麼簡單,因此實例化時間會多一些 //讓當前線程睡眠1毫秒 Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new TicketNumberHandler5(); } } return INSTANCE; } }
2,修改測試代碼
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); // TicketNumberHandler handler = TicketNumberHandler2.getInstance(); // TicketNumberHandler handler = TicketNumberHandler4.getInsatance(); TicketNumberHandler handler = TicketNumberHandler5.getInsatance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1117 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1204.1毫秒
十,測試第六種單例模式:雙重檢查
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler6 extends TicketNumberHandler { //保存單例實例對象 private static TicketNumberHandler6 INSTANCE; //私有化構造方法 private TicketNumberHandler6() {}; /** * 懶漢式,在第一次獲取單例對象的時候初始化對象 * @return */ public static TicketNumberHandler6 getInsatance() { //雙重檢查 if(INSTANCE == null) { synchronized (TicketNumberHandler5.class) { try { //這裏爲何要讓當前線程睡眠1毫秒呢? //由於在正常的業務邏輯中,單利模式的類不可能這麼簡單,因此實例化時間會多一些 //讓當前線程睡眠1毫秒 Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if(INSTANCE == null) { INSTANCE = new TicketNumberHandler6(); } } } return INSTANCE; } }
2,修改測試代碼
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); // TicketNumberHandler handler = TicketNumberHandler2.getInstance(); // TicketNumberHandler handler = TicketNumberHandler4.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler5.getInsatance(); TicketNumberHandler handler = TicketNumberHandler6.getInsatance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1041 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1117.1毫秒
十一,測試第七種單例模式:使用靜態內部類
1,單例代碼
package com.zcz.singleton; public class TicketNumberHandler7 extends TicketNumberHandler { //私有化構造器 public TicketNumberHandler7() {}; //靜態內部類 private static class TicketNumberHandler7Instance{ private static final TicketNumberHandler7 INSTANCE = new TicketNumberHandler7(); } public static TicketNumberHandler7 getInstance() { return TicketNumberHandler7Instance.INSTANCE; } }
2,修改測試代碼
Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); // TicketNumberHandler handler = TicketNumberHandler2.getInstance(); // TicketNumberHandler handler = TicketNumberHandler4.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler5.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler6.getInsatance(); TicketNumberHandler handler = TicketNumberHandler7.getInstance(); hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; };
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1250 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1184.4毫秒
十二,測試第八種單例模式:使用枚舉
1,單例代碼
package com.zcz.singleton; import java.util.concurrent.atomic.AtomicLong; public enum TicketNumberHandler8 { INSTANCE; private AtomicLong nextUniqueNumber = new AtomicLong(); //記錄下一個惟一的號碼 // private long nextUniqueNumber = 1; /** * 返回生成的號碼 * @return */ public synchronized Long getTicketNumber() { // return nextUniqueNumber++; return nextUniqueNumber.incrementAndGet(); } }
2,修改測試代碼
public static void main(String[] args) { // 用戶人數 int userNumber = 10000; // 保存用戶線程 Set<Thread> threadSet = new HashSet(); // 用於存放TicketNumberHandler實例對象 List<TicketNumberHandler8> hanlderList = new Vector(); // 保存生成的票號 List<Long> ticketNumberList = new Vector(); // 定義購票線程,一個線程模擬一個用戶 for(int i=0;i<userNumber;i++) { Thread t = new Thread() { public void run() { // TicketNumberHandler handler = TicketNumberHandler3.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler1.getInstance(); // TicketNumberHandler handler = TicketNumberHandler2.getInstance(); // TicketNumberHandler handler = TicketNumberHandler4.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler5.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler6.getInsatance(); // TicketNumberHandler handler = TicketNumberHandler7.getInstance(); TicketNumberHandler8 handler = TicketNumberHandler8.INSTANCE; hanlderList.add(handler); Long ticketNumber = handler.getTicketNumber(); ticketNumberList.add(ticketNumber); }; }; threadSet.add(t); } System.out.println("當前購票人數:"+threadSet.size()+" 人"); //記錄購票開始時間 long beginTime = System.currentTimeMillis(); for(Thread t : threadSet) { //開始購票 t.start(); } //記錄購票結束時間 long entTime; while(true) { //除去mian線程以外的全部線程結果後再記錄時間 if(Thread.activeCount() == 1) { entTime = System.currentTimeMillis(); break; } } //開始統計 System.out.println("票號生成類實例對象數目:"+new HashSet(hanlderList).size()); System.out.println("共出票:"+ticketNumberList.size()+"張"); System.out.println("實際出票:"+new HashSet(ticketNumberList).size()+"張"); System.out.println("出票用時:"+(entTime - beginTime)+" 毫秒"); }
3,測試結果
當前購票人數:10000 人 票號生成類實例對象數目:1 共出票:10000張 實際出票:10000張 出票用時:1031 毫秒
單例模式成功,出票數量正確,運行10次平均執行時間:1108毫秒
十三,總結
線程安全就再也不多說,除去第三種方式。其餘的均可以。
效率總結表:
單例模式名稱 | 平均十次執行時間(毫秒) |
第一種(使用靜態屬性,並初始化單例) | 1154.3 |
第二種(使用靜態代碼塊) | 1237.1 |
第四種(使用方法同步鎖) | 1091.86 |
第五種(使用同步代碼塊) | 1204.1 |
第六種(雙重檢查) | 1117.1 |
第七種(使用靜態內部類) | 1184.4 |
第八種(使用枚舉) | 1108 |
跟我預想的不一樣,沒有想到的是,居然是第四種方法的效率最高,極可能跟我測試數據的數量有關係(10000個用戶)。效率的話就很少作評論了,你們有興趣的話能夠本身親自試一下。別忘記告訴我測試的結果哦。
從代碼行數來看,使用枚舉是最代碼最少的方法了。
ok,這篇文章到這裏就結束了,雖然在效率上沒有結論,可是,在線程安全方面是明確了的。
相關java設計模式的文章:
JAVA設計模式-單例模式(Singleton)線程安全與效率
原創不易,轉載請註明出處:http://www.javashuo.com/article/p-alftwugu-cq.html