JAVA設計模式-單例模式(Singleton)線程安全與效率

一,前言

單例模式詳細大家都已經非常熟悉了,在文章單例模式的八種寫法比較中,對單例模式的概念以及使用場景都做了很不錯的說明。請在閱讀本文之前,閱讀一下這篇文章,因為本文就是按照這篇文章中的八種單例模式進行探索的。

本文的目的是:結合文章中的八種單例模式的寫法,使用實際的示例,來演示線程安全和效率

既然是實際的示例,那麼就首先定義一個業務場景:購票。大家都知道在春運的時候,搶票是非常激烈的。有可能同一張票就同時又成百上千的人同時在搶。這就對代碼邏輯的要求很高了,即不能把同一張票多次出售,也不能出現票號相同的票。

那麼,接下來我們就使用單例模式,實現票號的生成。同時呢在這個過程中利用上述文章中的八種單例模式的寫法,來實踐這八種單例模式的線程安全性和比較八種單例模式的效率。

既然文章中第三種單例模式(懶漢式)是線程不安全的,那麼我就從這個單例模式的實現開始探索一下線程安全。

因為不管是八種單例模式的實現方式的哪一種,票號的生成邏輯都是一樣的,所以,在此正式開始之前,為了更方便的編寫示例代碼,先做一些準備工作:封裝票號生成父類代碼。

二,封裝票號生成父類代碼

package com.zcz.singleton;
public class TicketNumberHandler {
//記錄下一個唯一的號碼
private long nextUniqueNumber = 1;
/**
* 返回生成的號碼
* @return
*/
public Long getTicketNumber() {
return nextUniqueNumber++;
}
}

票號的生成邏輯很簡單,就是一個遞增的整數,每獲取一次,就增加1。以後我們的每一種單例模式都繼承這個父類,就不用每一次都編寫這部分代碼,做到了代碼的重用。

接下來就是實現第三種單例模式,探索一下會不會引起線程安全問題。

三,實現第三種單例模式

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> 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)+" 毫秒");
}
}
/<usernumber>/<long>/<ticketnumberhandler>/<thread>

結合著代碼中的註釋,相信這部分測試代碼理解起來並不難,首先初始化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> 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)+" 毫秒");
}
/<usernumber>/<long>/<ticketnumberhandler8>/<thread>

3,測試結果

當前購票人數:10000 人
票號生成類實例對象數目:1
共出票:10000張
實際出票:10000張
出票用時:1031 毫秒

 單例模式成功,出票數量正確,運行10次平均執行時間:1108毫秒

十三,總結

線程安全就不再多說,除去第三種方式。其他的都可以。

效率總結表:

單例模式名稱平均十次執行時間(毫秒)第一種(使用靜態屬性,並初始化單例)1154.3第二種(使用靜態代碼塊)1237.1第四種(使用方法同步鎖)1091.86第五種(使用同步代碼塊)1204.1第六種(雙重檢查)1117.1第七種(使用靜態內部類)1184.4第八種(使用枚舉)1108

跟我預想的不同,沒有想到的是,竟然是第四種方法的效率最高,很可能跟我測試數據的數量有關係(10000個用戶)。效率的話就不多做評論了,大家有興趣的話可以自己親自試一下。別忘記告訴我測試的結果哦。

從代碼行數來看,使用枚舉是最代碼最少的方法了。

ok,這篇文章到這裡就結束了,雖然在效率上沒有結論,但是,在線程安全方面是明確了的。


分享到:


相關文章: