這篇文章將為大家詳細講解有關(guān)怎么在Java中實現(xiàn)雙重檢查加鎖單例模式,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
創(chuàng)新互聯(lián)公司是專業(yè)的新津縣網(wǎng)站建設(shè)公司,新津縣接單;提供成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè),網(wǎng)頁設(shè)計,網(wǎng)站設(shè)計,建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進行新津縣網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團隊,希望更多企業(yè)前來合作!
什么是DCL
DCL(Double-checked locking)被設(shè)計成支持延遲加載,當一個對象直到真正需要時才實例化:
class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) resource = new Resource(); return resource; } }
為什么需要推遲初始化?可能創(chuàng)建對象是一個昂貴的操作,有時在已知的運行中可能根本就不會去調(diào)用它,這種情況下能避免創(chuàng)建一個不需要的對象。延遲初始化能讓程序啟動更快。但是在多線程環(huán)境下,可能會被初始化兩次,所以需要把getResource()方法聲明為synchronized。不幸的是,synchronized方法比非synchronized方法慢100倍左右,延遲初始化的初衷是為了提高效率,但是加上synchronized后,提高了啟動速度,卻大幅下降了執(zhí)行時速度,這看起來并不是一樁好買賣。DCL看起來是最好的:
class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) { synchronized(this) { if (resource == null) resource = new Resource(); } } return resource; } }
延遲了初始化,又避免了競態(tài)條件??雌饋硎且粋€聰明的優(yōu)化--但它卻不能保證正常工作。為提高計算機系統(tǒng)性能,編譯器、處理器、緩存會對程序指令和數(shù)據(jù)進行重排序,而對象初始化操作并不是一個原子操作(可能會被重排序);因此可能存在這種情況:一個線程正在構(gòu)造對象過程中,另一個線程檢查時看見了resource的引用為非null。對象被非安全發(fā)布(逸出)。
根據(jù)Java內(nèi)存模型,synchronized的語義不僅僅是在同一個信號上的互斥(mutex),也包含線程和主存之間數(shù)據(jù)交互的同步,它確保在多處理器、多線程下對內(nèi)存能有可預(yù)見的一致性視圖。獲取或釋放鎖會觸發(fā)一次內(nèi)存屏障(memory barrier)--強迫線程本地內(nèi)存和主存同步。當一個線程退出一個synchronized block時,觸發(fā)一次寫屏障(write barrier )--在釋放鎖前必須把所有在這個同步塊里修改過的變量值刷新到主存;同樣,進入一個synchronized block時,觸發(fā)一次讀屏障(read barrier)--讓本地內(nèi)存失效,必須從主存中重新獲取在這個同步塊中將要引用的所有變量的值。正確使用同步能保證一個線程能以可預(yù)見的方式看到另一個線程的結(jié)果,線程對同步塊的操作就像是原子的?!罢_使用”的含義是:必須是在同一個鎖上同步。
DCL是怎么失效的
了解了JMM后,再來看看DCL是怎么失效的。DCL依賴于一個非同步的resource字段,看起來無害,實則不然。假如線程A進入了synchronized block,正在執(zhí)行resource = new Resource();此時線程B進入 getResource()??紤]到對象初始化在內(nèi)存上的影響:為new對象分配內(nèi)存;調(diào)用構(gòu)造方法,初始化對象的成員變量;把新創(chuàng)建好對象的引用賦值給SomeClass的resource字段。然而線程B沒有進入synchronized block,卻可能以不同于線程A執(zhí)行的順序看到上述內(nèi)存操作。B看到的可能是如下順序(指令重排序):分配內(nèi)存,把對象引用賦值給SomeClass的resource字段,調(diào)用構(gòu)造器。當內(nèi)存已經(jīng)分配好,A線程把SomeClass的resource字段設(shè)值完成后,線程B進入檢查發(fā)現(xiàn)resource不是null,跳過synchronized block返回一個未構(gòu)造完成的對象!顯而易見,結(jié)果不是預(yù)期的也不是想要的。
下面代碼是一個試圖修復(fù)DCL的加強版,遺憾的是它仍然不能保證正常工作。
// (Still) Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { Helper h; synchronized (this) { h = helper; if (h == null) synchronized (this) { h = new Helper(); } // release inner synchronization lock helper = h; } } return helper; } // other functions and members... }
這段代碼把Helper對象的構(gòu)造放在一個內(nèi)部的同步塊,又用了一個局部變量h來先接收初始化完成后的引用,直覺就是當這個內(nèi)部的同步塊退出時,應(yīng)該會觸發(fā)一次內(nèi)存屏障,能阻止對初始化Helper對象和給Foo的helper字段賦值的兩個操作重排序。不幸的是,直覺是完全錯誤的,對同步規(guī)則理解得不對。對于monitorexit規(guī)則(即,釋放同步),監(jiān)視器被釋放之前必須執(zhí)行monitorexit之前的動作。然而,沒有規(guī)定說monitorexit后的操作,不能在監(jiān)視器釋放前執(zhí)行。編譯器把賦值語句helper = h;移動到內(nèi)部同步塊之前是完全合理合法的,在這種情況下,我們又重新回到了以前。許多處理器提供執(zhí)行這種單向內(nèi)存屏障指令。改變語義要求釋放鎖是一個完整的內(nèi)存屏障會有性能損失。然而即使初始化時有一個完整的內(nèi)存屏障,也不能保證,在一些系統(tǒng)上,保證線程能看到helper的屬性字段的值為非null也需要同樣的內(nèi)存屏障。因為處理器有自己的本地緩存拷貝,某些處理器在執(zhí)行緩存一致性指令前,即使其他的處理器使用內(nèi)存屏障強制把最新值寫入主存,該處理器讀到的還是本地緩存拷貝的舊值。
關(guān)于重排序(reorder)有3種來源:編譯器、處理器、內(nèi)存系統(tǒng)。承諾“write-once, run-anywhere concurrent applications in Java” 的Java是接受處理器和內(nèi)存系統(tǒng)為優(yōu)化而重排序的,所以DCL單例模式?jīng)]有完美的解決方案,在多線程下編程要異常小心。下面討論多線程環(huán)境下單例模式的實現(xiàn)。
多線程環(huán)境下單例的實現(xiàn)
第一種,同步方法(synchronized)
優(yōu)點:所有情況下都能正常工作,延遲初始化;
缺點:同步嚴重損耗了性能,因為只有第一次實例化時才需要同步。
不推薦,絕大部分情況是沒必要延遲初始化的,不如采用急切實例化(eager initialization)
// Correct multithreaded version class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
第二種,使用IODH(Initialization On Demand Holder)
利用static塊做初始化,如下定義一個私有的靜態(tài)類去做初始化,或者直接在靜態(tài)塊代碼中去做初始化,能保證對象被正確構(gòu)造前對所有線程不可見。
class Foo { private static class HelperSingleton { public static Helper singleton = new Helper(); } public Helper getHelper() { return HelperSingleton.singleton; } // other functions and members... }
第三種,急切實例化(eager initialization)
class Foo { public static final Helper singleton = new Helper(); // other functions and members... } class Foo { private static final Helper singleton = new Helper(); public Helper getHelper() { return singleton; } // other functions and members... }
第四種,枚舉單例
public enum SingletonClass { INSTANCE; // other functions... }
上面4種方式在所有情況下都能保證正常工作
第五種,只對32位基本類型的值有效
缺陷:對64位的long和double及引用對象無效,因為64位的基本類型的賦值操作不是原子的。利用場景有限。
// Lazy initialization 32-bit primitives // Thread-safe if computeHashCode is idempotent class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) { h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }
第六種,DCL加上volatile語義
舊內(nèi)存模型(在JDK1.5發(fā)行之前)下失效,只能在JDK1.5后使用。
另外不推薦次方法,多核處理器下線程每次寫volatile字段都會把工作內(nèi)存及時刷新到主存,每次讀都會從主存獲取數(shù)據(jù),因為要和主存交換數(shù)據(jù),volatile的頻繁讀寫會占用數(shù)據(jù)總線資源。
// Works with acquire/release semantics for volatile // Broken under current semantics for volatile class Foo { private volatile Helper helper = null; public Helper getHelper() { Helper h = helper; if (helper == null) {// First check (no locking) synchronized (this) { h = helper; if (helper == null) helper = h = new Helper(); } } return helper; } }
第七種,不可變對象的單例
對于不可變對象(immutable object)本身是線程安全的,不需要同步,單例實現(xiàn)起來最簡單。比如Helper是一個不可變類型,只用用final修飾singleton字段就行:
class Foo { private final Helper singleton = new Helper(); public Helper getHelper() { return singleton; } // other functions and members... }
缺陷:舊內(nèi)存模型(在JDK1.5發(fā)行之前)下失效,只能在JDK1.5后使用,因為新內(nèi)存模型對final和volatile語義進行了加強。還有一個問題就是明確什么是不可變對象,如果對不可變對象含義不確定,請不要使用,另外當前是不可變對象不能保證將來此類一直是不可變對象(代碼總是在不斷修改),慎用!
關(guān)于怎么在Java中實現(xiàn)雙重檢查加鎖單例模式就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。