一塊兒學設計模式 - 單例模式

單例設計模式(Singleton Pattern)是最簡單且常見的設計模式之一,主要做用是提供一個全局訪問且只實例化一次的對象,避免多實例對象的狀況下引發邏輯性錯誤(實例化數量可控)...java

<!-- more -->git

概述

Java中,單例模式主要分三種:懶漢式單例、餓漢式單例、登記式單例三種。設計模式

  • 懶漢:非線程安全,須要用必定的風騷操做控制,裝逼失敗有可能致使看一週的海綿寶寶
  • 餓漢:天生線程安全,ClassLoad的時候就已經實例化好,該操做過於風騷會形成資源浪費
  • 單例註冊表:Spring初始化Bean的時候,默認單例用的就是該方式

特色安全

  • 私有構造方法,只能有一個實例。
  • 私有靜態引用指向本身實例,必須是本身在內部建立的惟一實例。
  • 單例類給其它對象提供的都是本身建立的惟一實例

案例性能優化

  • 在計算機系統中,內存、線程、CPU等使用狀況均可以再任務管理器中看到,但始終只能打開一個任務管理器,它在Windows操做系統中是具有惟一性的,由於彈多個框屢次採集數據浪費性能不說,採集數據存在偏差那就有點逗比了不是麼...
  • 每臺電腦只有一個打印機後臺處理程序
  • 線程池的設計通常也是採用單例模式,方便對池中的線程進行控制

注意事項微信

  • 實現方式種類較多,有的非線程安全方式的建立須要特別注意,且在使用的時候儘可能根據場景選取較優的,線程安全了還須要去考慮性能問題。
  • 不適用於變化的對象,若是同一類型的對象老是要在不一樣的用例場景發生變化,單例就會引發數據的錯誤,不能保存彼此的狀態。
  • 沒有抽象層,擴展有困難。
  • 職責太重,在必定程度上違背了單一職責原則
  • 使用時不能用反射模式建立單例,不然會實例化一個新的對象

解鎖姿式

第一種:單一檢查(懶漢)非線程安全多線程

public class LazyLoadBalancer {

    private static LazyLoadBalancer loadBalancer;
    private List<String> servers = null;

    private LazyLoadBalancer() {
        servers = new ArrayList<>();
    }

    public void addServer(String server) {
        servers.add(server);
    }

    public String getServer() {
        Random random = new Random();
        int i = random.nextInt(servers.size());
        return servers.get(i);
    }

    public static LazyLoadBalancer getInstance() {
        // 第一步:假設T1,T2兩個線程同時進來且知足 loadBalancer == null
        if (loadBalancer == null) {
            // 第二步:那麼 loadBalancer 即會被實例化2次
            loadBalancer = new LazyLoadBalancer();
        }
        return loadBalancer;
    }

    public static void main(String[] args) {
        LazyLoadBalancer balancer1 = LazyLoadBalancer.getInstance();
        LazyLoadBalancer balancer2 = LazyLoadBalancer.getInstance();
        System.out.println("hashCode:"+balancer1.hashCode());
        System.out.println("hashCode:"+balancer2.hashCode());
        balancer1.addServer("Server 1");
        balancer2.addServer("Server 2");
        IntStream.range(0, 5).forEach(i -> System.out.println("轉發至:" + balancer1.getServer()));
    }
}

日誌dom

hashCode:460141958
hashCode:460141958
轉發至:Server 2
轉發至:Server 2
轉發至:Server 2
轉發至:Server 1
轉發至:Server 2

分析: 在單線程環境一切正常,balancer1balancer2兩個對象的hashCode如出一轍,由此能夠判斷出堆棧中只有一分內容,不過該代碼塊中存在線程安全隱患,由於缺少競爭條件,多線程環境資源競爭的時候就顯得不太樂觀了,請看上文代碼註釋內容性能

第二種:無腦上鎖(懶漢)線程安全,性能較差,第一種升級版優化

public synchronized static LazyLoadBalancer getInstance() {
    if (loadBalancer == null) {
        loadBalancer = new LazyLoadBalancer();
    }
    return loadBalancer;
}

分析: 毫無疑問,知道synchronized關鍵字的都知道,同步方法在鎖沒釋放以前,其它線程都在排隊候着呢,想不安全都不行啊,但在安全的同時,性能方面就顯得短板了,我就初始化一次,你丫的每次來都上個鎖,不累的嗎(不要緊,它是爲了第三種作鋪墊的)..

第三種:雙重檢查鎖(DCL),徹底就是前兩種的結合體啊,有木有,只是將同步方法升級成了同步代碼塊

//劃重點了 **volatile**
private volatile static LazyLoadBalancer loadBalancer;

public static LazyLoadBalancer getInstance() {
    if (loadBalancer == null) {
        synchronized (LazyLoadBalancer.class) {
            if (loadBalancer == null) {
                loadBalancer = new LazyLoadBalancer();
            }
        }
    }
    return loadBalancer;
}

1.假設new LazyLoadBalancer()加載內容過多
2.因重排而致使loadBalancer提早不爲空
3.正好被其它線程觀察到對象非空直接返回使用

mem = allocate();                  //LazyLoadBalancer 分配內存
instance = mem;                     //注意當前實例已經不爲空了                      
initByLoadBalancer(instance);      //可是還有其它實例未初始化

存在問題: 首先咱們必定要清楚,DCL是不能保證線程安全的,稍微瞭解過JVM的就清楚,對比C/C++它始終缺乏一個正式的內存模型,因此爲了提高性能,它還會作一次指令重排操做,這個時候就會致使loadBalancer提早不爲空,正好被其它線程觀察到對象非空直接返回使用(但實際還有部份內容沒加載完成)

解決方案:volatile修飾loadBalancer,由於volatile修飾的成員變量能夠確保多個線程都可以順序處理,它會屏蔽JVM指令重排帶來的性能優化

volatile詳解:http://blog.battcn.com/2017/10/18/java/thread/thread-volatile/

第四種:Demand Holder (懶漢)線程安全,推薦使用

private LazyLoadBalancer() {}

private static class LoadBalancerHolder {
    //在JVM中 final 對象只會被實例化一次,沒法修改
    private final static LazyLoadBalancer INSTANCE = new LazyLoadBalancer();
}

public static LazyLoadBalancer getInstance() {
    return LoadBalancerHolder.INSTANCE;
}

分析:Demand Holder中,咱們在LazyLoadBalancer裏增長一個靜態(static)內部類,在該內部類中建立單例對象,再將
該單例對象經過getInstance()方法返回給外部使用,因爲靜態單例對象沒有做爲LazyLoadBalancer的成員變量直接實例化,類加載時並不會實例化LoadBalancerHolder,所以既能夠實現延遲加載,又能夠保證線程安全,不影響系統性能(居家旅行必備良藥啊)

第五種:枚舉特性(懶漢)線程安全

enum Lazy {
    INSTANCE;
    private LazyLoadBalancer loadBalancer;

    //枚舉的特性,在JVM中只會被實例化一次
    Lazy() {
        loadBalancer = new LazyLoadBalancer();
    }

    public LazyLoadBalancer getInstance() {
        return loadBalancer;
    }
}

分析: 相比上一種,該方式一樣是用到了JAVA特性:枚舉類保證只有一個實例(即便使用反射機制也沒法屢次實例化一個枚舉量)

第六種:餓漢單例(天生線程安全),

public class EagerLoadBalancer {
    private final static EagerLoadBalancer INSTANCE = new EagerLoadBalancer();

    private EagerLoadBalancer() {}

    public static EagerLoadBalancer getInstance() {
        return INSTANCE;
    }
}

分析: 利用ClassLoad機制,在加載時進行實例化,同時靜態方法只在編譯期間執行一次初始化,也就只有一個對象。使用的時候已被初始化完畢能夠直接調用,可是相比懶漢模式,它在使用的時候速度最快,但這玩意就像本身挖的坑哭着也得跳,你不用也得初始化一份在內存中佔個坑...

- 說點什麼

全文代碼:https://gitee.com/battcn/design-pattern/tree/master/Chapter2/battcn-singleton

  • 我的QQ:1837307557
  • battcn開源羣(適合新手):391619659

微信公衆號:battcn(歡迎調戲)

相關文章
相關標籤/搜索