單例模式,是特別常見的一種設計模式,因此我們有必要對它的概念和幾種常見的寫法非常了解,而且這也是面試中常問的知識點,
所謂單例模式,就是所有的請求都用一個物件來處理,如我們常用的Spring默認就是單例的,而多例模式是每一次請求都創建一個新的物件來處理,如structs2中的action,
使用單例模式,可以確保一個類只有一個實體,并且易于外部訪問,還可以節省系統資源,如果在系統中,希望某個類的物件只存在一個,就可以使用單例模式,
那怎么確保一個類只有一個實體呢?
我們知道,通常我們會通過new關鍵字來創建一個新的物件,這個時候類的建構式是public公有的,你可以隨意創建多個類的實體,所以,首先我們需要把建構式改為private私有的,這樣就不能隨意new物件了,也就控制了多個實體的隨意創建,
然后,定義一個私有的靜態屬性,來代表類的實體,它只能類內部訪問,不允許外部直接訪問,
最后,通過一個靜態的公有方法,把這個私有靜態屬性回傳出去,這就為系統創建了一個全域唯一的訪問點,
以上,就是單例模式的三個要素,總結為:
- 私有構造方法
- 指向自己實體的私有靜態變數
- 對外的靜態公共訪問方法
單例模式分為餓漢式和懶漢式,它們的主要區別就是,實體化物件的時機不同,餓漢式,是在類加載時就會實體化一個物件,懶漢式,則是在真正使用的時候才會實體化物件,
餓漢式單例代碼實作:
public class Singleton {
// 餓漢式單例,直接創建一個私有的靜態實體
private static Singleton singleton = new Singleton();
//私有構造方法
private Singleton(){
}
//提供一個對外的靜態公有方法
public static Singleton getInstance(){
return singleton;
}
}
懶漢式單例代碼實作
public class Singleton {
// 懶漢式單例,類加載時先不創建實體
private static Singleton singleton = null;
//私有構造方法
private Singleton(){
}
//真正使用時才創建類的實體
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
稍有經驗的程式員就發現了,以上懶漢式單例的實作方式,在單執行緒下是沒有問題的,但是,如果在多執行緒中使用,就會發現它們回傳的實體有可能不是同一個,我們可以通過代碼來驗證一下,創建十個執行緒,分別啟動,執行緒內去獲得類的實體,把實體的 hashcode 列印出來,只要相同則認為是同一個實體;若不同,則說明創建了多個實體,
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new MyThread().start();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}
}
/**
運行多次,就會發現,hashcode會出現不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/
所以,以上懶漢式的實作方式是執行緒不安全的,那餓漢式呢?你可以手動測驗一下,會發現不管運行多少次,回傳的hashcode都是相同的,因此,認為餓漢式單例是執行緒安全的,
那為什么餓漢式就是執行緒安全的呢?這是因為,餓漢式單例在類加載時,就創建了類的實體,也就是說在執行緒去訪問單例物件之前就已經創建好實體了,而一個類在整個生命周期中只會被加載一次,因此,也就可以保證實體只有一個,所以說,餓漢式單例天生就是執行緒安全的,(可以了解一下類加載機制)
既然懶漢式單例不是執行緒安全的,那么我們就需要去改造一下,讓它在多執行緒環境下也能正常作業,以下介紹幾種常見的寫法:
- 使用synchronized方法
實作非常簡單,只需要在方法上加一個synchronized關鍵字即可
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
//使用synchronized修飾方法,即可保證執行緒安全
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這種方式,雖然可以保證執行緒安全,但是同步方法的作用域太大,鎖的粒度比較粗,因此,執行效率就比較低,
- synchronized 同步塊
既然,同步整個方法的作用域大,那我縮小范圍,在方法里邊,只同步創建實體的那一小部分代碼塊不就可以了嗎(因為方法較簡單,所以鎖代碼塊和鎖方法沒什么明顯區別),
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//synchronized只修飾方法內部的部分代碼塊
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
這種方法,本質上和第一種沒什么區別,因此,效率提升不大,可以忽略不計,
- 雙重檢測(double check)
可以看到,以上的第二種方法只要呼叫getInstance方法,就會走到同步代碼塊里,因此,會對效率產生影響,其實,我們完全可以先判斷實體是否已經存在,若已經存在,則說明已經創建好實體了,也就不需要走同步代碼塊了;若不存在即為空,才進入同步代碼塊,這樣可以提高執行效率,因此,就有以下雙重檢測了:
public class Singleton {
//注意,此變數需要用volatile修飾以防止指令重排序
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//進入方法內,先判斷實體是否為空,以確定是否需要進入同步代碼塊
if(singleton == null){
synchronized (Singleton.class){
//進入同步代碼塊時也需要判斷實體是否為空
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
需要注意的一點是,此方式中,靜態實體變數需要用volatile修飾,因為,new Singleton() 是一個非原子性操作,其流程為:
a.給 singleton 實體分配記憶體空間
b.呼叫Singleton類的建構式創建實體
c.將 singleton 實體指向分配的記憶體空間,這時認為singleton實體不為空
正常順序為 a->b->c,但是,jvm為了優化編譯程式,有時候會進行指令重排序,就會出現執行順序為 a->c->b,這在多執行緒中就會表現為,執行緒1執行了new物件操作,然后發生了指令重排序,會導致singleton實體已經指向了分配的記憶體空間(c),但是實際上,實體還沒創建完成呢(b),
這個時候,執行緒2就會認為實體不為空,判斷 if(singleton == null)為false,于是不走同步代碼塊,直接回傳singleton實體(此時拿到的是未實體化的物件),因此,就會導致執行緒2的物件不可用而使用時報錯,
4)使用靜態內部類
思考一下,由于類加載是按需加載,并且只加載一次,所以能保證執行緒安全,這也是為什么說餓漢式單例是天生執行緒安全的,同樣的道理,我們是不是也可以通過定義一個靜態內部類來保證類屬性只被加載一次呢,
public class Singleton {
private Singleton(){
}
//靜態內部類
private static class Holder {
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
//呼叫內部類的屬性,獲取單例物件
return Holder.singleton;
}
}
而且,JVM在加載外部類的時候,不會加載靜態內部類,只有在內部類的方法或屬性(此處即指singleton實體)被呼叫時才會加載,因此不會造成空間的浪費,
5)使用列舉類
因為列舉類是執行緒安全的,并且只會加載一次,所以利用這個特性,可以通過列舉類來實作單例,
public class Singleton {
private Singleton(){
}
//定義一個列舉類
private enum SingletonEnum {
//創建一個列舉實體
INSTANCE;
private Singleton singleton;
//在列舉類的構造方法內實體化單例類
SingletonEnum(){
singleton = new Singleton();
}
private Singleton getInstance(){
return singleton;
}
}
public static Singleton getInstance(){
//獲取singleton實體
return SingletonEnum.INSTANCE.getInstance();
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/26068.html
標籤:設計模式
下一篇:備忘錄模式
