怎么在Java中實(shí)現(xiàn)雙重檢查加鎖單例模式

這篇文章將為大家詳細(xì)講解有關(guān)怎么在Java中實(shí)現(xiàn)雙重檢查加鎖單例模式,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。

創(chuàng)新互聯(lián)公司是專業(yè)的新津縣網(wǎng)站建設(shè)公司,新津縣接單;提供成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè),網(wǎng)頁設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行新津縣網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!

什么是DCL

DCL(Double-checked locking)被設(shè)計(jì)成支持延遲加載,當(dāng)一個(gè)對象直到真正需要時(shí)才實(shí)例化:

class SomeClass {
 private Resource resource = null;
 public Resource getResource() {
 if (resource == null)
  resource = new Resource();
 return resource;
 }
}

為什么需要推遲初始化?可能創(chuàng)建對象是一個(gè)昂貴的操作,有時(shí)在已知的運(yùn)行中可能根本就不會(huì)去調(diào)用它,這種情況下能避免創(chuàng)建一個(gè)不需要的對象。延遲初始化能讓程序啟動(dòng)更快。但是在多線程環(huán)境下,可能會(huì)被初始化兩次,所以需要把getResource()方法聲明為synchronized。不幸的是,synchronized方法比非synchronized方法慢100倍左右,延遲初始化的初衷是為了提高效率,但是加上synchronized后,提高了啟動(dòng)速度,卻大幅下降了執(zhí)行時(shí)速度,這看起來并不是一樁好買賣。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)條件。看起來是一個(gè)聰明的優(yōu)化--但它卻不能保證正常工作。為提高計(jì)算機(jī)系統(tǒng)性能,編譯器、處理器、緩存會(huì)對程序指令和數(shù)據(jù)進(jìn)行重排序,而對象初始化操作并不是一個(gè)原子操作(可能會(huì)被重排序);因此可能存在這種情況:一個(gè)線程正在構(gòu)造對象過程中,另一個(gè)線程檢查時(shí)看見了resource的引用為非null。對象被非安全發(fā)布(逸出)。

根據(jù)Java內(nèi)存模型,synchronized的語義不僅僅是在同一個(gè)信號上的互斥(mutex),也包含線程和主存之間數(shù)據(jù)交互的同步,它確保在多處理器、多線程下對內(nèi)存能有可預(yù)見的一致性視圖。獲取或釋放鎖會(huì)觸發(fā)一次內(nèi)存屏障(memory barrier)--強(qiáng)迫線程本地內(nèi)存和主存同步。當(dāng)一個(gè)線程退出一個(gè)synchronized block時(shí),觸發(fā)一次寫屏障(write barrier )--在釋放鎖前必須把所有在這個(gè)同步塊里修改過的變量值刷新到主存;同樣,進(jìn)入一個(gè)synchronized block時(shí),觸發(fā)一次讀屏障(read barrier)--讓本地內(nèi)存失效,必須從主存中重新獲取在這個(gè)同步塊中將要引用的所有變量的值。正確使用同步能保證一個(gè)線程能以可預(yù)見的方式看到另一個(gè)線程的結(jié)果,線程對同步塊的操作就像是原子的?!罢_使用”的含義是:必須是在同一個(gè)鎖上同步。

DCL是怎么失效的

了解了JMM后,再來看看DCL是怎么失效的。DCL依賴于一個(gè)非同步的resource字段,看起來無害,實(shí)則不然。假如線程A進(jìn)入了synchronized block,正在執(zhí)行resource = new Resource();此時(shí)線程B進(jìn)入 getResource()??紤]到對象初始化在內(nèi)存上的影響:為new對象分配內(nèi)存;調(diào)用構(gòu)造方法,初始化對象的成員變量;把新創(chuàng)建好對象的引用賦值給SomeClass的resource字段。然而線程B沒有進(jìn)入synchronized block,卻可能以不同于線程A執(zhí)行的順序看到上述內(nèi)存操作。B看到的可能是如下順序(指令重排序):分配內(nèi)存,把對象引用賦值給SomeClass的resource字段,調(diào)用構(gòu)造器。當(dāng)內(nèi)存已經(jīng)分配好,A線程把SomeClass的resource字段設(shè)值完成后,線程B進(jìn)入檢查發(fā)現(xiàn)resource不是null,跳過synchronized block返回一個(gè)未構(gòu)造完成的對象!顯而易見,結(jié)果不是預(yù)期的也不是想要的。

下面代碼是一個(gè)試圖修復(fù)DCL的加強(qiáng)版,遺憾的是它仍然不能保證正常工作。

// (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)造放在一個(gè)內(nèi)部的同步塊,又用了一個(gè)局部變量h來先接收初始化完成后的引用,直覺就是當(dāng)這個(gè)內(nèi)部的同步塊退出時(shí),應(yīng)該會(huì)觸發(fā)一次內(nèi)存屏障,能阻止對初始化Helper對象和給Foo的helper字段賦值的兩個(gè)操作重排序。不幸的是,直覺是完全錯(cuò)誤的,對同步規(guī)則理解得不對。對于monitorexit規(guī)則(即,釋放同步),監(jiān)視器被釋放之前必須執(zhí)行monitorexit之前的動(dòng)作。然而,沒有規(guī)定說monitorexit后的操作,不能在監(jiān)視器釋放前執(zhí)行。編譯器把賦值語句helper = h;移動(dòng)到內(nèi)部同步塊之前是完全合理合法的,在這種情況下,我們又重新回到了以前。許多處理器提供執(zhí)行這種單向內(nèi)存屏障指令。改變語義要求釋放鎖是一個(gè)完整的內(nèi)存屏障會(huì)有性能損失。然而即使初始化時(shí)有一個(gè)完整的內(nèi)存屏障,也不能保證,在一些系統(tǒng)上,保證線程能看到helper的屬性字段的值為非null也需要同樣的內(nèi)存屏障。因?yàn)樘幚砥饔凶约旱谋镜鼐彺婵截?,某些處理器在?zhí)行緩存一致性指令前,即使其他的處理器使用內(nèi)存屏障強(qiáng)制把最新值寫入主存,該處理器讀到的還是本地緩存拷貝的舊值。

關(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)境下單例模式的實(shí)現(xiàn)。

多線程環(huán)境下單例的實(shí)現(xiàn)

第一種,同步方法(synchronized)

優(yōu)點(diǎn):所有情況下都能正常工作,延遲初始化;

缺點(diǎn):同步嚴(yán)重?fù)p耗了性能,因?yàn)橹挥械谝淮螌?shí)例化時(shí)才需要同步。

不推薦,絕大部分情況是沒必要延遲初始化的,不如采用急切實(shí)例化(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塊做初始化,如下定義一個(gè)私有的靜態(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...
}

第三種,急切實(shí)例化(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及引用對象無效,因?yàn)?4位的基本類型的賦值操作不是原子的。利用場景有限。

// 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字段都會(huì)把工作內(nèi)存及時(shí)刷新到主存,每次讀都會(huì)從主存獲取數(shù)據(jù),因?yàn)橐椭鞔娼粨Q數(shù)據(jù),volatile的頻繁讀寫會(huì)占用數(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)本身是線程安全的,不需要同步,單例實(shí)現(xiàn)起來最簡單。比如Helper是一個(gè)不可變類型,只用用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后使用,因?yàn)樾聝?nèi)存模型對final和volatile語義進(jìn)行了加強(qiáng)。還有一個(gè)問題就是明確什么是不可變對象,如果對不可變對象含義不確定,請不要使用,另外當(dāng)前是不可變對象不能保證將來此類一直是不可變對象(代碼總是在不斷修改),慎用!

關(guān)于怎么在Java中實(shí)現(xiàn)雙重檢查加鎖單例模式就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

文章名稱:怎么在Java中實(shí)現(xiàn)雙重檢查加鎖單例模式
文章URL:http://bm7419.com/article20/jcspjo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設(shè)計(jì)公司、域名注冊、網(wǎng)站改版移動(dòng)網(wǎng)站建設(shè)、軟件開發(fā)網(wǎng)站收錄

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)