java dcl 失效解决_DCL失效原因和解决方案
Java內存模型? ? 在了解Java的同步秘密之前,先來看看JMM(Java Memory Model)。
Java被設計為跨平臺的語言,在內存管理上,顯然也要有一個統一的模型。而且Java語言最大的特點就是廢除了指針,把程序員從痛苦中解脫出來,不用再考慮內存使用和管理方面的問題。
可惜世事總不盡如人意,雖然JMM設計上方便了程序員,但是它增加了虛擬機的復雜程度,而且還導致某些編程技巧在Java語言中失效。
JMM主要是為了規定了線程和內存之間的一些關系。對Java程序員來說只需負責用synchronized同步關鍵字,其它諸如與線程/內存之間進行數據交換/同步等繁瑣工作均由虛擬機負責完成。根據JMM的設計,系統存在一個主內存(Main Memory),Java中所有變量都儲存在主存中,對于所有線程都是共享的。每條線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內存中進行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。
線程若要對某變量進行操作,必須經過一系列步驟:首先從主存復制/刷新數據到工作內存,然后執行代碼,進行引用/賦值操作,最后把變量內容寫回Main Memory。Java語言規范(JLS)中對線程和主存互操作定義了6個行為,分別為load,save,read,write,assign和use,這些操作行為具有原子性,且相互依賴,有明確的調用先后順序。具體的描述請參見JLS第17章。
我們在前面的章節介紹了synchronized的作用,現在,從JMM的角度來重新審視synchronized關鍵字。
假設某條線程執行一個synchronized代碼段,其間對某變量進行操作,JVM會依次執行如下動作:
(1) 獲取同步對象monitor (lock)
(2) 從主存復制變量到當前工作內存 (read and load)
(3) 執行代碼,改變共享變量值 (use and assign)
(4) 用工作內存數據刷新主存相關內容 (store and write)
(5) 釋放同步對象鎖 (unlock)
可見,synchronized的另外一個作用是保證主存內容和線程的工作內存中的數據的一致性。如果沒有使用synchronized關鍵字,JVM不保證第2步和第4步會嚴格按照上述次序立即執行。因為根據JLS中的規定,線程的工作內存和主存之間的數據交換是松耦合的,什么時候需要刷新工作內存或者更新主內存內容,可以由具體的虛擬機實現自行決定。如果多個線程同時執行一段未經synchronized保護的代碼段,很有可能某條線程已經改動了變量的值,但是其他線程卻無法看到這個改動,依然在舊的變量值上進行運算,最終導致不可預料的運算結果。
DCL失效? ?針對延遲加載法的同步實現所產生的性能低的問題,我們可以采用DCL,即雙重檢查加鎖(Double Check Lock)的方法來避免每次調用getInstance()方法時都同步。
在開始討論之前,先介紹一下LazyLoad,這種技巧很常用,就是指一個類包含某個成員變量,在類初始化的時候并不立即為該變量初始化一個實例,而是等到真正要使用到該變量的時候才初始化之。
例如下面的代碼:class Foo {
private Resource res = null;
public Resource getResource() {
if (res == null)
res = new Resource();
return res;
}
}? ?由于LazyLoad可以有效的減少系統資源消耗,提高程序整體的性能,所以被廣泛的使用,連Java的缺省類加載器也采用這種方法來加載Java類。
在單線程環境下,一切都相安無事,但如果把上面的代碼放到多線程環境下運行,那么就可能會出現問題。假設有2條線程,同時執行到了if(res == null),那么很有可能res被初始化2次,為了避免這樣的Race Condition,得用synchronized關鍵字把上面的方法同步起來。代碼如下:
Class Foo {
private Resource res = null;
public synchronized Resource getResource() {
if (res == null)
res = new Resource();
return res;
}
}? ?synchronized過的方法在速度上要比未同步的方法慢上100倍,同時你也發現,只有第一次調用該方法的時候才需要同步,而一旦res初始化完成,同步完全沒必要。所以你很快就把代碼重構成了下面的樣子:
Class Foo {
private Resource res = null;
private Date d = new Data();
public Resource getResource() {
if (res == null){ //(1)
synchronized(Foo.class){
if(res == null){
res = new Resource(); //(2)
}
}
}
return res;
}
}Double-Checked Locking看起來是非常完美的。但是很遺憾,根據Java的語言規范,上面的代碼是不可靠的。
出現上述問題, 最重要的2個原因如下:
1, 編譯器優化了程序指令, 以加快cpu處理速度.
2, 多核cpu動態調整指令順序, 以加快并行運算能力.
問題出現的順序:
1, 線程A, 發現對象未實例化, 準備開始實例化
2, 由于編譯器優化了程序指令, 允許對象在構造函數未調用完前, 將共享變量的引用指向部分構造的對象, 雖然對象未完全實例化, 但已經不為null了.
3, 線程B, 發現部分構造的對象已不是null, 則直接返回了該對象.無法保證語句(2)和語句(1)不存在happen-before(詳解跳轉)關系.
一個線程A運行到"這里"時,A的工作區中,肯定已經產生一個Foo對象,而且這時這個對象已經完成了Data d.現在線程A調用時間到,執行權被切換到另一個線程B來執行,會有什么問題呢?如果res不為null,線程B獲得了一個res,但可能res.getD()卻還沒有初始化.
對于"這里"這條語句,線程A還沒有離開同步塊.因為沒有"離開同步塊"這個條件,線程A的工作區沒有強制與主存儲器同步,這時工作區中有兩個字段res,d。雖然在線程A的工作區res和d都是完整的,但有JSL沒有強制不允許先把res映射到主存儲區,如果哪個jvm實現按它的優化方案先把工作存儲器中的res同步到主存儲器了,這時正好線程B獲取了,而d卻沒有同步過去,那么線程B就獲取了res的引用卻找不能res.getD()。
DCL解決方案1.DCL的替代 Initialize-On-Demand
Class ResSingleton {
public static Resource res = new Resource();
}
這里LazyFoo只有一個靜態成員變量。當第一次使用ResSingleton.res的時候,JVM才會初始化一個Resource實例,并且JVM會保證初始化的結果及時寫入主存,能讓其他線程看到,這樣就成功的實現了LazyLoad。
2、另外,可以將instance聲明為volatile(詳解跳轉),即
private volatile static LazySingleton instance;
在讀線程B讀一個volatile變量后,寫線程A在寫這個volatile變量之前,所有可見的共享變量的值都將立即變得對讀線程B可見。
參考:http://www.blogjava.net/weidagang2046/articles/3494.html
總結
以上是生活随笔為你收集整理的java dcl 失效解决_DCL失效原因和解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第四十八期:只因写了一段爬虫,公司200
- 下一篇: java学习(90):Character