一、概念
java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這裏主要介紹三種:懶漢式單例、餓漢式單例。
單例模式有以下特點:
- 單例模式確保當前系統只有一個實例
- 單例類必須自己創建自己的唯一實例
- 單例類必須給其他對象提供這一實例
單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了避免不一致狀態,避免政出多頭。
二、代碼實現
懶漢式單例
//懶漢式單例類.在第一次調用的時候實例化自己
public class Singleton {
private Singleton() {}
private static Singleton single=null;
//靜態工廠方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
Singleton通過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠實例化構造方法爲private的類的,那基本上會使所有的Java單例實現失效。)
但是以上懶漢式單例的實現沒有考慮線程安全問題,它是線程不安全的,併發環境下很可能出現多個Singleton實例,要實現線程安全,有以下方式,都是對getInstance這個方法改造,保證了懶漢式單例的線程安全。
1、getInstance方法上加同步
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
在方法調用上加了同步,雖然線程安全了,但是每次都要同步,會影響性能,畢竟大多數情況下是不需要同步的。
2、雙重檢查鎖定
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
爲什麼需要兩次if判斷呢?
因爲在高併發時,兩個線程AB都同時進入了第一個if判斷,若A搶到了鎖,實例化了一個對象,釋放鎖,然後B在搶到鎖。此時如果不做第二次if判斷的話,B線程將會再實例化一個對象。
在getInstance中做了兩次null檢查,確保了只有第一次調用單例的時候纔會做同步,這樣也是線程安全的,同時避免了每次都同步的性能損耗。
但這種雙重檢查鎖定也是不安全的,不安全在這行代碼上,singleton=new Singleton(),這一行代碼可以分解爲三行僞代碼
memory=allocate(); //1:分配對象的內存空間
ctorInstance(memory) //2: 初始化對象
instance=memory; //3:設置instance指向剛分配的內存地址
上面的3行僞代碼中,第二行和第三行可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3重排序之後的執行時序如下。
memory=allocate(); //1:分配對象的內存空間
instance=memory; //2:設置instance指向剛分配的內存地址
ctorInstance(memory) //3: 初始化對象
產生不安全的例子如下
多線程執行時序表
時間 | 線程A | 線程B |
t1 | A1:分配對象的內存空間 | |
t2 | A3:設置instance指向內存空間 | |
t3 | B1:判斷instance是否爲空 | |
t4 | B2:由於instance不爲null,線程B將訪問instance引用對象 | |
t5 | A2:初始化對象 | |
t6 | A4:訪問instance引用對象 |
3、基於volatile的單例
public class SafeDoubleCheckedLocking{
private volatile static Instance instance
public static Instance getInstance(){
synchonized(SafeDoubleCheckedLocking.class){
if(instance==null){
instance=new Instance();//instance爲volatile,這個操作具有原子性。不會導致重排序
}
}
}
}
4、基於類初始化的單例
原理:JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
public class InstanceFactory{
private static class InstanceHolder{
public static Instance instance=new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance();//這裏將導致InstanceHolder類被初始化.
}
}
JVM在類初始化期間會獲取這個初始化鎖,並且每個線程至少獲取一次鎖來確保這個類已經被初始化了。
5、餓漢式單例
//餓漢式單例類.在類初始化時,已經自行實例化
public class Singleton1 {
private Singleton1() {}
private static final Singleton1 single = new Singleton1();
//靜態工廠方法
public static Singleton1 getInstance() {
return single;
}
}
餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不再改變,所以天生是線程安全的。
三、資源加載和性能
餓漢式在類創建的同時就實例化一個靜態對象出來,不管之後會不會使用這個單例,都會佔據一定的內存,但是相應的,在第一次調用時速度也會更快,因爲其資源已經初始化完成。
而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候纔會實例化對象出來,第一次調用時要做初始化,如果要做的工作比較多,性能上會有些延遲,之後就和餓漢式一樣了。
轉載地址:https://blog.csdn.net/jason0539/article/details/23297037/