DoubleCheck双重检查实战及原理解析
生活随笔
收集整理的這篇文章主要介紹了
DoubleCheck双重检查实战及原理解析
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
那這種方式兼顧了性能和線程安全,而且也是懶加載的,那現在我們來創建一個類,這個類名也是懶加載的,雙重檢查首先從上至下是一個時間,線程0,因為剛剛呢,我們debug的時候,是從0開始的,所以現在演示的是一個單線程的情況,注意1234這幾個步驟,首先分配對象的內存空間,然后正常來說要初始化這個對象,然后設置instance指向內存空間,這個instance就是我們代碼里面lazyDoubleCheckSingleton,只所以沒有命名成instance,是為了下載到源碼之后,很好的區分,所以我們每個單例模式里面,對象的命名都會和單例模式有關,然后初始訪問對象,這個初次訪問對象,也就是線程0開始訪問這個對象了,所以2和3怎么換順序,4都是在最后,2和3的重排序,對結果并沒有影響,但是重排序并不是百分百命中的,是有一定概率的,但是這種隱患我們一定要消除,這個是單線程沒有什么問題,看一下多線程模擬一下
首先時間還是從上至下,線程0從左側劃分,右側是線程1,首先第一個時間點分配對象的內存空間,假如線程0開始重排序了,先設置了instance指向了內存空間,這個時候線程1從上至下判斷instance是否為null,這個時候判斷出來了,instance并不為Null,因為他有指向內存空間,然后線程1開始訪問對象,也就是線程1比線程0更早的訪問對象,所以線程1訪問到的對象呢,是一個在線程0中還沒有被初始化成的對象,那這個時候就有問題了,這個對象并沒有被完整的初始化上,系統就要報異常了,那對于2和3的重排序剛剛也說了,并不影響線程0的第4個步驟,訪問了這個對象,我們知道了問題所在,我們怎么解決呢,我們可以不允許2和3重排序,或者允許線程0的2和3重排序,但是不允許線程1看到這個重排序,那我們首先可以讓2和3不允許重排序
package com.learn.design.pattern.creational.singleton;/*** DoubleCheck關注的是什么呢* 雙重檢查* 在哪里檢查* * * @author Leon.Sun**/
public class LazyDoubleCheckSingleton {/*** 我們聲明volatile* 我們只要做這么一個小小的修改* 就可以實現線程安全的延遲初始化* 這樣重排序就可以被禁止* 那在多線程的時候呢* CPU也有共享內存* 我們在加了volatile關鍵字之后* 所有線程都能夠看到共享內存的執行狀態* 保證了內存的可見性* 那這里面就和多線程有關了* 關于volatile修飾的共享變量呢* 在進行寫操作的時候* 會多出一些匯編代碼* 起到兩個作用* 第一是將當前處理器緩存好的數據緩存到數據內存* 那這個寫回到內存的操作呢* 回寫到其他內存緩存了* 該內存的地址數據無效* 那因為其他CPU內存數據無效了* 所以他們又從共享內存共享數據* 這樣呢就保證了內存的可見性* 這里面主要是用了緩存一致性協議* 那當處理器發現我這個緩存已經無效了* 所以我在進行操作的時候* 會重新從系統內存中把數據讀到處理器的緩存里* 那我們就不深入講解這塊了* 再講就要到匯編和信號的問題了* 我們的重點還是單例模式* 通過volatile和doublecheck這種方式呢* 既兼顧了性能又兼顧了線程* */private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;private LazyDoubleCheckSingleton(){}/*** 首先我們的方法不用鎖了* 也就是說public static LazyDoubleCheckSingleton getInstance()這個方法一調到這里* 就立刻鎖上* 而是把鎖定放在方法體中* 對象還是進行一個判斷* 判斷完成之后* 這個時候呢* * Thread0在if(lazyDoubleCheckSingleton == null)這里* 這個instance是null* 我們切到Thread1上* Thread1進入if* 第二重判斷* 我們重點關注DoubleCheck* 我們再切換到Thread0* 因為lazyDoubleCheckSingleton為空* 所以Thread0也可以進來* 但是在synchronized (LazyDoubleCheckSingleton.class)會被block掉* 那我們再切換到Thread1上* Thread1單步走* 開始new* 這個時候已經new完了* Thread1現在釋放了這個鎖* 所以切回到Thread0* Thread0獲取這個鎖之后* 進入第二個if判斷* 這個時候進入第二層的空判斷* 那他判斷lazyDoubleCheckSingleton這個對象并不為空* 所以他直接走到這里* 然后在單步走* 走到return* 注意他后面是425* 而Thread1也是425* 因為是debug* 執行非常快* 因為我們通過volitale關鍵字已經去new對象的時候* 有可能出現的重排序已經解決了* 那我們直接F6單步走* 現在我們看一下console* 并且在創建對象的時候* 希望能理解doublecheck* 單例模式的一個演進* 一定要學會多線程debug的實戰技能* 非常重要* 那剛剛我們也說了* 對這種重排序* 我們有兩種解決方案* 第一是不允許2和3重排序* 還有一種方案允許23重排序* 但是不允許其他線程看到這個重排序* 剛剛我們是基于不允許23重排序來解決的* 接下來我們使用第二種方式來解決這個問題* 同時講解一下原理* * * * * * @return*/public static LazyDoubleCheckSingleton getInstance(){if(lazyDoubleCheckSingleton == null){/*** 判斷完成之后我們鎖定這個單例的這個類* synchronized (LazyDoubleCheckSingleton.class)* 注意這里* 我們現在鎖定了這個類* 那也就代表著if是進來的* 至少進到這個里面的線程到if(lazyDoubleCheckSingleton == null)這里的時候* 這個對象還是空的* 那我們想象一下* 因為if(lazyDoubleCheckSingleton == null)這里沒有鎖* 所以如果另外一個線程進來* 如果判斷if(lazyDoubleCheckSingleton == null)它為空* 那到synchronized (LazyDoubleCheckSingleton.class)* 也會阻塞* 如果進入到這里面的* 已經把這個對象生成好了* 那剛剛新進來的線程呢* if(lazyDoubleCheckSingleton == null)判斷的時候* 會直接return* 這里面還有一個小坑* 就是指定重排序的問題* 那一會講到這里再說* 加鎖之后我們肯定還要做一層空的判斷* 這個lazyDoubleCheckSingleton對象如果為null的話* 這個時候我才會給他賦值* lazyDoubleCheckSingleton這個對象我們平時使用的時候* 一般命名成instance* 只不過我們這里要講多種方式* 通過命名也加以區分* 免得弄混了* 這個instance就是單例的對象* 那我們看一下現在的這種寫法* synchronized (LazyDoubleCheckSingleton.class)不加鎖* 不為null就直接返回* 如果為null的話也只有一個線程進入到這里面* 這個就可以大量的減少synchronized加載方法上的時候帶來的性能開銷* 看上去我們的這個實現非常完美* 多個線程的時候我們通過加鎖來保證只有 一個線程來創建對象* 當對象創建好之后呢* 以后再調用getInstance方法的時候* 都不會再需要加鎖* 直接返回已創建好的對象* 這里面有個隱患* 那隱患出在上面的if判斷* 還有lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();* 現在說一下為什么* 首先在上面的if判斷的時候* 雖然判斷了這個對象是不是為空* 這個時候是有可能不為空的* 雖然他不為空* 但是這個對象可能還沒有完成初始化* 也就是lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();還沒有執行完成* 那我們來看一下這個new的這塊代碼* 當我把這個對象new出來的時候* 看上去是一行* 實際上這里面經歷了三個步驟* 我們加一個注釋寫到這里吧* 第一步分配內存給這個對象* 也就是給這個對象分配內存* 第二步初始化這個對象* 第三步設置lazyDoubleCheckSingleton指向剛分配的內存地址* 也就是這一行執行了三個操作* 那在2和3的時候* 可能會被重排序* 也就是說呢* 2和3的順序有可能會被顛倒* 變成這樣的* 先分配這個內存給這個對象* 然后單例對象指向剛分配的內存地址* 注意現在已經指向了這個內存地址* 所以這里空判斷的時候呢* 并不為空* 但是這個單例對象有可能沒有初始化完成* 這里面就要說一下* JAVA語言規范里面有說* 所有線程在執行JAVA程序時* 必須遵守intra-thread semantics這么一個約定* 他保證重排序不會改變單線程內的程序執行結果* 例如123這幾個執行步驟* 對于單線程來說* 2和3互換位置* 其實不會改變單線程的執行結果* 所以JAVA語言規范允許那些在單線程內不會改變單線程執行結果的重排序* 也就是說2和3是允許的* 因為這個重排序可以提高程序的執行性能* 為了更好地理解呢* 畫了一個圖給大家看一下* * * * */synchronized (LazyDoubleCheckSingleton.class){if(lazyDoubleCheckSingleton == null){lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();//1.分配內存給這個對象
// //3.設置lazyDoubleCheckSingleton 指向剛分配的內存地址//2.初始化對象
// intra-thread semantics
// ---------------//3.設置lazyDoubleCheckSingleton 指向剛分配的內存地址}}}return lazyDoubleCheckSingleton;}
}
package com.learn.design.pattern.creational.singleton;public class T implements Runnable {@Overridepublic void run() {
// LazySingleton lazySingleton = LazySingleton.getInstance();
// System.out.println(Thread.currentThread().getName()+" "+lazySingleton);/*** 調用他的getInstance方法* */LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
// StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();;// ContainerSingleton.putInstance("object",new Object());
// Object instance = ContainerSingleton.getInstance("object");
// ThreadLocalInstance instance = ThreadLocalInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);}
}
package com.learn.design.pattern.creational.singleton;public class T implements Runnable {@Overridepublic void run() {
// LazySingleton lazySingleton = LazySingleton.getInstance();
// System.out.println(Thread.currentThread().getName()+" "+lazySingleton);/*** 調用他的getInstance方法* */LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
// StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();;// ContainerSingleton.putInstance("object",new Object());
// Object instance = ContainerSingleton.getInstance("object");
// ThreadLocalInstance instance = ThreadLocalInstance.getInstance();System.out.println(Thread.currentThread().getName()+" "+instance);}
}
?
總結
以上是生活随笔為你收集整理的DoubleCheck双重检查实战及原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 单列设计模式 懒汉式及多线程debug
- 下一篇: 单例设计模式-静态内部类-基于类初始化的