設計模式:單例模式
1.基本概念
目的: 保證類在內存中只有一個對象,可以直接訪問,不需要實例化該類的對象
注意:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
使用場景:
1、要求生產唯一序列號。
2、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。
注意事項: getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。
2.代碼實現
2.1.餓漢式(即時創建對象)
這種方式比較常用,但容易產生垃圾對象。空間換時間。
是否多線程安全: 是
實現難度: 易
優點: 沒有加鎖,執行效率會提高。
缺點: 類加載時就初始化,浪費內存。
public class Main {
public static void main(String[] args) {
//通過方法獲得唯一對象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//判斷是否爲唯一對象
System.out.println(s1 == s2);//運行結果爲true
}
}
class Singleton {
//1.私有構造方法,其他類不能訪問該構造方法
private Singleton() {}
//2.創建本類對象
private static Singleton s = new Singleton();
//3.對外提供公共的訪問方法
public static Singleton getInstance() {//獲取實例
return s;
}
}
2.2.懶漢式(使用時才創建對象)
2.2.1.線程不安全的懶漢式
這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因爲沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式,不要求線程安全,在多線程不能正常工作。
是否多線程安全: 否
實現難度: 易
public class Main {
public static void main(String[] args) {
//通過方法獲得唯一對象
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
class SingletonLazy {
//1.私有構造方法,其他類不能訪問該構造方法
private SingletonLazy() {}
//2.聲明一個引用
private static SingletonLazy s;
//3.對外提供公共的訪問方法
public static SingletonLazy getInstance() {//獲取實例
//當沒有對象時,進行創建
if(s==null){
s = new SingletonLazy();
}
return s;
}
}
2.2.2.線程安全的懶漢式
這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
是否多線程安全: 是
實現難度: 易
優點: 第一次調用才初始化,避免內存浪費。
缺點: 必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
public class Main {
public static void main(String[] args) {
//通過方法獲得唯一對象
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
class SingletonLazy {
//1.私有構造方法,其他類不能訪問該構造方法
private SingletonLazy() {}
//2.聲明一個引用
private static SingletonLazy s;
//3.對外提供公共的訪問方法(使用synchronized鎖,防止線程搶佔)
public static synchronized SingletonLazy getInstance() {//獲取實例
//當沒有對象時,進行創建
if(s==null){
s = new SingletonLazy();
}
return s;
}
}
2.3.final實現方式(瞭解)
通過final關鍵字,實現對象在創建之後不可被更改。
public class Main {
public static void main(String[] args) {
//獲得唯一對象
Singletonfinal s1 = Singletonfinal.s;
Singletonfinal s2 = Singletonfinal.s;
System.out.println(s1 == s2);//結果爲true
}
}
class Singletonfinal{
//1.私有構造方法,其他類不能訪問該構造方法
private Singletonfinal() {}
//2.聲明一個引用,final表示了地址不可被修改
public static final Singletonfinal s = new Singletonfinal();
}
3.雙重校驗鎖(DCL,即double-checked locking)
3.1.DCL代碼實現
是否 Lazy 初始化: 是
是否多線程安全: 是
實現難度: 較複雜
描述: 這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。getInstance() 的性能對應用程序很關鍵。
public class Main {
public static void main(String[] args) {
SingletonDCL s1 = SingletonDCL.getInstance();
SingletonDCL s2 = SingletonDCL.getInstance();
System.out.println(s1 == s2);//結果是true
}
}
class SingletonDCL {
//1.構造私有方法
private SingletonDCL() {}
//2.懶漢式Lazy初始化,使用volatile可以保證可見性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部獲得實例的方法
public static SingletonDCL getInstance() {
//如果實例未創建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通過字節碼,反射機制獲得對象,並上鎖
//加上鎖後再次判斷,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回對象
}
}
3.2.使用volatile的原因(禁止指令重排序)
指令重排序: 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
例如:
singletonDCL = new SingletonDCL();//實例化對象這行代碼
這行代碼實際上可以分解成三個步驟:
1.分配內存空間。
2.初始化對象。
3.將對象指向剛分配的內存空間。
但是有些編譯器因爲性能的原因,可能會改變2和3的順序,就成了:
1.分配內存空間。
2.將對象指向剛分配的內存空間。
3.初始化對象。
在不使用volatile且發生重排序的情況下,調用順序如下
線程一 | 線程二 |
---|---|
檢查到singletonDCL爲null | - |
獲取鎖 | - |
再次檢查到singletonDCL爲null | - |
爲singletonDCL分配內存空間 | - |
將singletonDCL指向內存空間 | - |
- | 檢查到singletonDCL不爲空 |
- | 訪問singletonDCL(此時線程一還未初始化完成對象) |
初始化singletonDCL | - |
在這種情況下,在線程二訪問singletonDCL時,訪問的是一個初始化未完成的對象。