摘要:單例是一個類,我們希望該類的對象在任意高并發多線程的調用下,只被初始化一次例如用于系統環境和詞典的加載等,后續線程均直接調用即可。我們首先來講解幾種常見的單例模式和其優缺點吧。更多單例模式可以看這里。線程進入區域,此時還未啟動。
寫此文初衷源于昨晚線上代碼拋出的一個空指針異常。單例是一個類,我們希望該類的對象在任意高并發多線程的調用下,只被初始化一次(例如用于系統環境和詞典的加載等),后續線程均直接調用即可。我們首先來講解幾種常見的單例模式和其優缺點吧。
1.懶漢式// 懶漢式,線程不安全 class SingletonDemo { // 定義一個私有的靜態全局變量來保存該類的唯一實例 private static SingletonDemo instance; private SingletonDemo() { } public static SingletonDemo getInstance() { // 這里可以保證只實例化一次 if (instance == null) { //語句(1) instance = new SingletonDemo(); } return instance; } }
以上代碼很明顯不能滿足我們的要求。設想有n個線程同時執行語句(1),此時實例還未被初始化,因此均判斷為null,于是這n個線程每一個都新建了該類對象。
2.懶漢式改進// 懶漢式,線程安全,但不高效,因為任何時候只能有一個線程調用getInstance()方法。 class SingletonDemo2 { private static SingletonDemo2 instance; private SingletonDemo2() {} public static synchronized SingletonDemo2 getInstance() { //語句(1) if (instance == null) { //區域(1) instance = new SingletonDemo2(); } return instance; } }
我們使用synchronized來強制使每個線程串行執行語句(1),因此永遠只有第一個線程新建了該類對象。那么這段代碼缺點在哪呢?速度慢。假設現在有1000個線程,均在語句(1)處排隊,當第一個線程創建新對象后,剩下999個線程仍然需要排隊,進入區域(1)判斷不為空并返回。
這里懶漢式的意思是:要用的時候才去new。區別于接下來要講的:
3.餓漢式/** * 餓漢式,單例的實例被聲明成static和final,在第一次加載到內存中時會初始化。 * * 缺點: * 不是一種懶加載(lazy initlalization),在一些場景中無法使用: * 譬如Singleton實例的創建時以來參數或者配置文件的,在getInstance()之前必須調用 */ class SingletonDemo5 { // 類加載時就初始化 private static final SingletonDemo5 instance = new SingletonDemo5(); private SingletonDemo5() {} public static SingletonDemo5 getInstance() { return instance; } }
這段代碼利用了jvm對private static final只初始化一次的特性,可以解決多線程問題,但是當我們要在getInstance()前做一些配置工作(例如初始化數據庫連接等),那么這種方式就捉襟見肘了。
4.雙重檢驗鎖// 雙重檢驗鎖(double checked) class SingletonDemo3 { private static volatile SingletonDemo3 instance; private SingletonDemo3() {} public static SingletonDemo3 getInstance() { if (instance == null) { // 區域(1) synchronized (SingletonDemo3.class) { // 區域(2) if (instance == null) { instance = new SingletonDemo3(); // 語句(1) } } } return instance; } }
雙重檢驗鎖(double checked)。注意到它有2次判斷,一次在同步塊內,一次在同步塊外。假設現在有4個線程T1,T2,T3,T4。T1,T2進入了區域(1),T3,T4還沒啟動。T1能進入區域(2)創建instance成功,之后T2進入區域(2),判斷非空并出來。此時T3,T4啟動了,不會進入區域(1),且無需等待鎖。
instance變量聲明成volatile, 它可以禁止指令重排序優化。
volatile的兩個作用:
禁止指令重排優化。
所修飾的變量一旦被修改,其他線程立即可見。(但是非原子操作,即其他線程可以感知到變量被修改,但無法使用val += 1這種語句使其原子增加。)因此可用于1讀者n寫者的場景,例如點擊"游戲退出按鈕",其他(金幣累加/音效)線程將立即感知到。
更多單例模式可以看這里。
5.異常問題回放昨晚報出異常的代碼片段如下:
class WrongSample { private volatile static WrongSample instance; private WrongSample() { } public static WrongSample getInstance() { if (instance == null) { // 區域(1) synchronized (WrongSample.class) { // 區域(2) if (instance == null) { instance = new WrongSample(); // 語句(1) instance.init(); //語句(2) } } } return instance; } private void init() { } }
該代碼使用雙重檢驗鎖構建了一個單例,且對單例進行初始化。那么空指針異常拋出對原因在哪呢?設想現在有線程T1,T2。線程T1進入區域(2),T2此時還未啟動。T1執行了語句(1),但并未執行語句2,此時instance已經不是null,所以T2啟動時在區域(1)判斷非null將直接返回instance,但T2并未被初始化,由是產生異常。
解決方案:初始化操作放進構造函數,執行語句(1)時里暗含里執行構造函數。代碼如下:
class WrongSample { private volatile static WrongSample instance; private WrongSample() { init(); } public static WrongSample getInstance() { if (instance == null) { // 區域(1) synchronized (WrongSample.class) { // 區域(2) if (instance == null) { instance = new WrongSample(); // 語句(1) } } } return instance; } private void init() { } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70418.html
摘要:使用靜態類體現的是基于對象,而使用單例設計模式體現的是面向對象。二編寫單例模式的代碼編寫單例模式的代碼其實很簡單,就分了三步將構造函數私有化在類的內部創建實例提供獲取唯一實例的方法餓漢式根據上面的步驟,我們就可以輕松完成創建單例對象了。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:關于對于重排序的講解,強烈推薦閱讀程曉明寫的深入理解內存模型二重排序。語義語義單線程下,為了優化可以對操作進行重排序。編譯器和處理器為單個線程實現了語義,但對于多線程并不實現語義。雙重加載的單例模式分析即雙重檢查加鎖。 版權聲明:本文由吳仙杰創作整理,轉載請注明出處:https://segmentfault.com/a/1190000009231182 1. 引言 在開始分析雙重加鎖單...
摘要:如果需要防范這種攻擊,請修改構造函數,使其在被要求創建第二個實例時拋出異常。單例模式與單一職責原則有沖突。源碼地址參考文獻設計模式之禪 定義 單例模式是一個比較簡單的模式,其定義如下: 保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。 或者 Ensure a class has only one instance, and provide a global point of ac...
摘要:總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。二餓漢式單例餓漢式單例類在類初始化時,已經自行實例化靜態工廠方法餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以天生是線程安全的。 概念: Java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這里主要介紹兩種:懶漢式單例、餓漢式單例。 單例模式有以下特點: 1、單例類只能有一個實例。 ...
摘要:一般來說,這種單例實現有兩種思路,私有構造器,枚舉。而這種方式又分了飽漢式,餓漢式。通過關鍵字防止指令重排序。什么是單例?為什么要用單例? 一個類被設計出來,就代表它表示具有某種行為(方法),屬性(成員變量),而一般情況下,當我們想使用這個類時,會使用new關鍵字,這時候jvm會幫我們構造一個該類的實例。而我們知道,對于new這個關鍵字以及該實例,相對而言是比較耗費資源的。所以如果我們能夠想...
閱讀 2866·2021-11-11 10:58
閱讀 1920·2021-10-11 10:59
閱讀 3489·2019-08-29 16:23
閱讀 2324·2019-08-29 11:11
閱讀 2785·2019-08-28 17:59
閱讀 3838·2019-08-27 10:56
閱讀 2049·2019-08-23 18:37
閱讀 3111·2019-08-23 16:53