单列设计模式 懒汉式及多线程debug
生活随笔
收集整理的這篇文章主要介紹了
单列设计模式 懒汉式及多线程debug
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
我們也是遵循演進(jìn)的一個方式,一點(diǎn)點(diǎn)體會他們的不同,以及優(yōu)缺點(diǎn),單例模式也是創(chuàng)建型模式,我們在這個包下創(chuàng)建一個包,我們先學(xué)習(xí)一下懶漢式單例模式
package com.learn.design.pattern.creational.singleton;/*** * @author Leon.Sun**/
public class LazySingleton {/*** 首先我們聲明一個靜態(tài)的單例的一個對象* 懶漢式可以理解說他比較懶* 在初始化的時候呢* 是沒有創(chuàng)建的* 而是做一個延遲加載* 為了不讓外部來進(jìn)行new* 這兩個點(diǎn)都比較好理解* * */private static LazySingleton lazySingleton = null;private LazySingleton(){if(lazySingleton != null){throw new RuntimeException("單例構(gòu)造器禁止反射調(diào)用");}}/*** 我們寫一個獲取LazySingleton的一個方法* 他呢肯定是public的* getInstance* 這里面很簡單* * 第一種方式我們在getInstance方法上* 加上synchronized這個關(guān)鍵字* 是這個方法變成同步方法* 如果這個鎖加載靜態(tài)方法上* 相當(dāng)于鎖的是LazySingleton這個類的clas文件* 如果這個不是靜態(tài)方法呢* 相當(dāng)于鎖是在堆內(nèi)存中生成的對象* 這里要注意一下* 也就是說在靜態(tài)方法中* 我們加了synchronized這個關(guān)鍵字* 來鎖這個方法的時候* 相當(dāng)于鎖了這個類* 那我們換一種寫法* * 這個時候Thread0進(jìn)入這個方法* 我們再切入到Thread1上* 可以看到斷點(diǎn)并沒有往下跳* 同時這個線程的狀態(tài)變成Monitor* 那我們可以認(rèn)為Thread1現(xiàn)在是阻塞狀態(tài)* 這個是一個監(jiān)視鎖* 我們可以看到下邊有一個提示* 這個提示很清晰* Thead1被線程0給阻塞了* 下面有一個藍(lán)色的按鈕* 如果點(diǎn)的話* 就會繼續(xù)線程0* 所以Thread1進(jìn)不了getInstance的方法* 我們再切回Thread0* 單步走返回了* 然后準(zhǔn)備輸出了* 我們再切回Thread1* 我們可以看到Thread1是Running狀態(tài)* F6單步* 這個時候我們看一下* 因?yàn)樵趇f(lazySingleton == null)的時候* Thread0已經(jīng)new完了* LazySingleton并不等于空* 他們兩返回的是同一個對象* 并且在LazySingleton里面* 這個時候只生產(chǎn)了一個實(shí)例* F8直接過* 看一下console* 這兩個拿到的也是同一個對象* 通過這種同步的方式* 我們解決了懶漢式在多線程可能引起的問題* 那我們也知道synchronized比較消耗資源* 這里面有一個加鎖和解鎖的一個開銷* 而且synchronized修飾static方法的時候* 鎖的是這個class* 這個鎖的范圍也是非常大的* 對性能也會有一定的影響* 那我們還有沒有一種方式繼續(xù)演進(jìn)* 在性能和安全性方面取得平衡* 答案是有的* 那我們現(xiàn)在繼續(xù)演進(jìn)我們的懶漢式* * * * @return*/public synchronized static LazySingleton getInstance(){
// public static LazySingleton getInstance(){/*** 剛剛寫靜態(tài)鎖的這種寫法和現(xiàn)在這種寫法是一樣的* synchronized (LazySingleton.class)這里鎖的也是LazySingleton* 那么再復(fù)原成同步方法* public synchronized static LazySingleton getInstance()* 就是這個樣子的* * */
// synchronized (LazySingleton.class) {/*** 做一個空判斷* 如果lazySingleton為null的話* 給他賦值* new一個LazySingleton* * 最開始的時候lazySingleton為null* 因?yàn)榕袛鄉(xiāng)azySingleton == null結(jié)果為true* 進(jìn)入到lazySingleton = new LazySingleton();* 進(jìn)入之后還沒有執(zhí)行new* 這個時候執(zhí)行l(wèi)azySingleton = new LazySingleton();* 這個單例就有值了* * 我們單步來到if(lazySingleton == null)* debug來調(diào)試多線程的節(jié)奏* 來觸發(fā)這種寫法在多線程中的問題* 那之前run的時候沒有發(fā)生這種問題呢* 那我們看到的是表象沒有發(fā)生* 但是實(shí)際有沒有發(fā)生其實(shí)是不確定的* 另外一種就是實(shí)際就沒有發(fā)生* 但是對于發(fā)生了但是我們沒有看到* 這種情況只要我們接著往下看* 肯定就理解了* 那另外一個他沒有發(fā)生* 這個呢也很好理解* 他就是沒有發(fā)生* 我說的發(fā)生是多線程的問題* 也就是這個單例模式是否只new出來一個對象* 那如果我們沒有用斷點(diǎn)干預(yù)* 直接run的話* 和CPU分配的時間有關(guān)* 是有一定概率的* 我們現(xiàn)在程序比較簡單* 如果我們程序復(fù)雜一些* 這種隱患還是有可能發(fā)生的* 既然有隱患我們就要消除掉他* 那現(xiàn)在我們繼續(xù)來看一下他* 把這個隱患找出來* 一定要掌握多線程debug* 講TOMCAT集群的時候呢* 操作了都進(jìn)程debug* 現(xiàn)在在設(shè)計(jì)模式的課程中* 我們在寫多線程debug* 這個技能非常重要* 一定要學(xué)會* * 因?yàn)門hread0并沒有賦值上* 所以他還是為空* F6單步* 他也進(jìn)來了* 那我再切回Thread0* * 我們切換到Thread0線程上* 為null進(jìn)來* * 單步Thread1也進(jìn)來了* 現(xiàn)在對于Thread1我直接讓他返回* 已經(jīng)輸出了* 那我們再切回Thread0上* 這個時候Thread1是431* 但是lazySingleton = new LazySingleton()一旦完成* 那這個對象就變了* 已經(jīng)變成了432* 這個時候他就返回* 然后輸出* 我們再看一下console* 那這個時候我們就可以看到* Thread1拿到的42D這個對象* Thread0是拿到420這個對象* 所以呢我們不能被表面所迷惑* 例如我們直接run的時候* 看到的對象是同一個* 但是在這中間獲取的可能不止一個對象* 所以呢這個是有一定概率的* 例如在lazySingleton里面* 如果第一個線程執(zhí)行特別快* 先new上了* 那第二個線程判斷為null的時候* 就會返回false* 然后直接return* 所以呢* 具體這個返回值什么樣子* 都是有一定概率的* 那這個隱患我們是一定要消除的* 怎么消除呢* 很簡單* 對于懶漢式這種方式呢* 首先我們來到lazySingleton里邊* * */if(lazySingleton == null){/*** 第一個線程到這里的時候并且沒有執(zhí)行這一行* 第二個線程達(dá)到if(lazySingleton == null)這里* 那if(lazySingleton == null)這一行判斷* 因?yàn)閘azySingleton = new LazySingleton();這里還沒有new* 那么if(lazySingleton == null)判斷的結(jié)果是true* 所以第二個線程也會進(jìn)入到lazySingleton = new LazySingleton();* 那這個對象就new了兩次* 同時會返回最后執(zhí)行的lazySingleton* 那我們怎么驗(yàn)證呢* 我們寫一個測試類* * 懶漢式注重的是延遲加載* * 這個時候lazySingleton還是null* 因?yàn)閘azySingleton賦值new LazySingleton()* 這一行還沒有執(zhí)行完* 所以他并沒有被復(fù)制上* 這個時候我們切到Thread1上* Thread1單步* * 可以看到在lazySingleton = new LazySingleton()還沒有執(zhí)行的時候* lazySingleton是有值的* 但是我們馬上就要執(zhí)行它了* F6單步* 現(xiàn)在可以看到lazySingleton值變了* 也就是說在我們寫的懶漢式的單例模式中* lazySingleton在多線程的模式中* 生成了不止一個實(shí)例* 那現(xiàn)在是兩個線程* 如果是多個線程呢* 所以在第一次初始化的時候* 有可能創(chuàng)建很多個的單例對象* 如果這個單例類的對象特別消耗資源* 那很有可能造成系統(tǒng)故障* 這個呢是非常有風(fēng)險(xiǎn)的* 那在我們這個例子中l(wèi)azySingleton并沒有特別消耗資源的地方* 但是場景是一樣的* 那我們現(xiàn)在再切換到Thread0上* lazySingleton這個對象已經(jīng)被Thread1重新賦值了* 那現(xiàn)在F8過* 主動跳到主線程* 我們看一下console* 這個時候我們可以看到* Thread0和Thread1拿到的還是同一個對象* * 現(xiàn)在Thread0在lazySingleton = new LazySingleton();這一行上* 我們再切換到Thread1上* * */lazySingleton = new LazySingleton();}
// }/*** 把這個對象返回* 把這個對象返回回去* 這種方式是線程不安全的* 我們看一看代碼* 在單線程的時候* 這種模式這種寫法* 是OK的* 但是一旦多線程來使用這個單例的話* 假設(shè)我們現(xiàn)在兩個線程* * 這個時候Thread0已經(jīng)把lazySingleton賦值上了* 這個時候我們在切到Thread1上* * */return lazySingleton;}// public static void main(String[] args) throws Exception {
// Class objectClass = LazySingleton.class;
// Constructor c = objectClass.getDeclaredConstructor();
// c.setAccessible(true);
///*** 通過LazySingleton這個類調(diào)用getInstance方法* 因?yàn)樗莗rivate構(gòu)造器* 所以在外部是new不到他的* 然后我們直接getInstance* * 這樣簡單一個單線程獲取的單例呢就完成了* 只有使用它的時候才初始化* 如果不使用就初始化LazySingleton對象* 那main本身是一個線程* 現(xiàn)在我們在這個線程中再創(chuàng)建兩個線程* 去獲取現(xiàn)在這種寫法的單例的時候* 會碰到什么問題呢* 我們一起來看一下* 首先我們寫一下線程的類* * * */
// LazySingleton o1 = LazySingleton.getInstance();
// System.out.println("Program end.....");
//
// Field flag = o1.getClass().getDeclaredField("flag");
// flag.setAccessible(true);
// flag.set(o1,true);
//
//
// LazySingleton o2 = (LazySingleton) c.newInstance();
//
// System.out.println(o1);
// System.out.println(o2);
// System.out.println(o1==o2);
// }}
package com.learn.design.pattern.creational.singleton;/*** * @author Leon.Sun**/
public class LazySingleton {/*** 首先我們聲明一個靜態(tài)的單例的一個對象* 懶漢式可以理解說他比較懶* 在初始化的時候呢* 是沒有創(chuàng)建的* 而是做一個延遲加載* 為了不讓外部來進(jìn)行new* 這兩個點(diǎn)都比較好理解* * */private static LazySingleton lazySingleton = null;private LazySingleton(){if(lazySingleton != null){throw new RuntimeException("單例構(gòu)造器禁止反射調(diào)用");}}/*** 我們寫一個獲取LazySingleton的一個方法* 他呢肯定是public的* getInstance* 這里面很簡單* * 第一種方式我們在getInstance方法上* 加上synchronized這個關(guān)鍵字* 是這個方法變成同步方法* 如果這個鎖加載靜態(tài)方法上* 相當(dāng)于鎖的是LazySingleton這個類的clas文件* 如果這個不是靜態(tài)方法呢* 相當(dāng)于鎖是在堆內(nèi)存中生成的對象* 這里要注意一下* 也就是說在靜態(tài)方法中* 我們加了synchronized這個關(guān)鍵字* 來鎖這個方法的時候* 相當(dāng)于鎖了這個類* 那我們換一種寫法* * 這個時候Thread0進(jìn)入這個方法* 我們再切入到Thread1上* 可以看到斷點(diǎn)并沒有往下跳* 同時這個線程的狀態(tài)變成Monitor* 那我們可以認(rèn)為Thread1現(xiàn)在是阻塞狀態(tài)* 這個是一個監(jiān)視鎖* 我們可以看到下邊有一個提示* 這個提示很清晰* Thead1被線程0給阻塞了* 下面有一個藍(lán)色的按鈕* 如果點(diǎn)的話* 就會繼續(xù)線程0* 所以Thread1進(jìn)不了getInstance的方法* 我們再切回Thread0* 單步走返回了* 然后準(zhǔn)備輸出了* 我們再切回Thread1* 我們可以看到Thread1是Running狀態(tài)* F6單步* 這個時候我們看一下* 因?yàn)樵趇f(lazySingleton == null)的時候* Thread0已經(jīng)new完了* LazySingleton并不等于空* 他們兩返回的是同一個對象* 并且在LazySingleton里面* 這個時候只生產(chǎn)了一個實(shí)例* F8直接過* 看一下console* 這兩個拿到的也是同一個對象* 通過這種同步的方式* 我們解決了懶漢式在多線程可能引起的問題* 那我們也知道synchronized比較消耗資源* 這里面有一個加鎖和解鎖的一個開銷* 而且synchronized修飾static方法的時候* 鎖的是這個class* 這個鎖的范圍也是非常大的* 對性能也會有一定的影響* 那我們還有沒有一種方式繼續(xù)演進(jìn)* 在性能和安全性方面取得平衡* 答案是有的* 那我們現(xiàn)在繼續(xù)演進(jìn)我們的懶漢式* * * * @return*/public synchronized static LazySingleton getInstance(){
// public static LazySingleton getInstance(){/*** 剛剛寫靜態(tài)鎖的這種寫法和現(xiàn)在這種寫法是一樣的* synchronized (LazySingleton.class)這里鎖的也是LazySingleton* 那么再復(fù)原成同步方法* public synchronized static LazySingleton getInstance()* 就是這個樣子的* * */
// synchronized (LazySingleton.class) {/*** 做一個空判斷* 如果lazySingleton為null的話* 給他賦值* new一個LazySingleton* * 最開始的時候lazySingleton為null* 因?yàn)榕袛鄉(xiāng)azySingleton == null結(jié)果為true* 進(jìn)入到lazySingleton = new LazySingleton();* 進(jìn)入之后還沒有執(zhí)行new* 這個時候執(zhí)行l(wèi)azySingleton = new LazySingleton();* 這個單例就有值了* * 我們單步來到if(lazySingleton == null)* debug來調(diào)試多線程的節(jié)奏* 來觸發(fā)這種寫法在多線程中的問題* 那之前run的時候沒有發(fā)生這種問題呢* 那我們看到的是表象沒有發(fā)生* 但是實(shí)際有沒有發(fā)生其實(shí)是不確定的* 另外一種就是實(shí)際就沒有發(fā)生* 但是對于發(fā)生了但是我們沒有看到* 這種情況只要我們接著往下看* 肯定就理解了* 那另外一個他沒有發(fā)生* 這個呢也很好理解* 他就是沒有發(fā)生* 我說的發(fā)生是多線程的問題* 也就是這個單例模式是否只new出來一個對象* 那如果我們沒有用斷點(diǎn)干預(yù)* 直接run的話* 和CPU分配的時間有關(guān)* 是有一定概率的* 我們現(xiàn)在程序比較簡單* 如果我們程序復(fù)雜一些* 這種隱患還是有可能發(fā)生的* 既然有隱患我們就要消除掉他* 那現(xiàn)在我們繼續(xù)來看一下他* 把這個隱患找出來* 一定要掌握多線程debug* 講TOMCAT集群的時候呢* 操作了都進(jìn)程debug* 現(xiàn)在在設(shè)計(jì)模式的課程中* 我們在寫多線程debug* 這個技能非常重要* 一定要學(xué)會* * 因?yàn)門hread0并沒有賦值上* 所以他還是為空* F6單步* 他也進(jìn)來了* 那我再切回Thread0* * 我們切換到Thread0線程上* 為null進(jìn)來* * 單步Thread1也進(jìn)來了* 現(xiàn)在對于Thread1我直接讓他返回* 已經(jīng)輸出了* 那我們再切回Thread0上* 這個時候Thread1是431* 但是lazySingleton = new LazySingleton()一旦完成* 那這個對象就變了* 已經(jīng)變成了432* 這個時候他就返回* 然后輸出* 我們再看一下console* 那這個時候我們就可以看到* Thread1拿到的42D這個對象* Thread0是拿到420這個對象* 所以呢我們不能被表面所迷惑* 例如我們直接run的時候* 看到的對象是同一個* 但是在這中間獲取的可能不止一個對象* 所以呢這個是有一定概率的* 例如在lazySingleton里面* 如果第一個線程執(zhí)行特別快* 先new上了* 那第二個線程判斷為null的時候* 就會返回false* 然后直接return* 所以呢* 具體這個返回值什么樣子* 都是有一定概率的* 那這個隱患我們是一定要消除的* 怎么消除呢* 很簡單* 對于懶漢式這種方式呢* 首先我們來到lazySingleton里邊* * */if(lazySingleton == null){/*** 第一個線程到這里的時候并且沒有執(zhí)行這一行* 第二個線程達(dá)到if(lazySingleton == null)這里* 那if(lazySingleton == null)這一行判斷* 因?yàn)閘azySingleton = new LazySingleton();這里還沒有new* 那么if(lazySingleton == null)判斷的結(jié)果是true* 所以第二個線程也會進(jìn)入到lazySingleton = new LazySingleton();* 那這個對象就new了兩次* 同時會返回最后執(zhí)行的lazySingleton* 那我們怎么驗(yàn)證呢* 我們寫一個測試類* * 懶漢式注重的是延遲加載* * 這個時候lazySingleton還是null* 因?yàn)閘azySingleton賦值new LazySingleton()* 這一行還沒有執(zhí)行完* 所以他并沒有被復(fù)制上* 這個時候我們切到Thread1上* Thread1單步* * 可以看到在lazySingleton = new LazySingleton()還沒有執(zhí)行的時候* lazySingleton是有值的* 但是我們馬上就要執(zhí)行它了* F6單步* 現(xiàn)在可以看到lazySingleton值變了* 也就是說在我們寫的懶漢式的單例模式中* lazySingleton在多線程的模式中* 生成了不止一個實(shí)例* 那現(xiàn)在是兩個線程* 如果是多個線程呢* 所以在第一次初始化的時候* 有可能創(chuàng)建很多個的單例對象* 如果這個單例類的對象特別消耗資源* 那很有可能造成系統(tǒng)故障* 這個呢是非常有風(fēng)險(xiǎn)的* 那在我們這個例子中l(wèi)azySingleton并沒有特別消耗資源的地方* 但是場景是一樣的* 那我們現(xiàn)在再切換到Thread0上* lazySingleton這個對象已經(jīng)被Thread1重新賦值了* 那現(xiàn)在F8過* 主動跳到主線程* 我們看一下console* 這個時候我們可以看到* Thread0和Thread1拿到的還是同一個對象* * 現(xiàn)在Thread0在lazySingleton = new LazySingleton();這一行上* 我們再切換到Thread1上* * */lazySingleton = new LazySingleton();}
// }/*** 把這個對象返回* 把這個對象返回回去* 這種方式是線程不安全的* 我們看一看代碼* 在單線程的時候* 這種模式這種寫法* 是OK的* 但是一旦多線程來使用這個單例的話* 假設(shè)我們現(xiàn)在兩個線程* * 這個時候Thread0已經(jīng)把lazySingleton賦值上了* 這個時候我們在切到Thread1上* * */return lazySingleton;}// public static void main(String[] args) throws Exception {
// Class objectClass = LazySingleton.class;
// Constructor c = objectClass.getDeclaredConstructor();
// c.setAccessible(true);
///*** 通過LazySingleton這個類調(diào)用getInstance方法* 因?yàn)樗莗rivate構(gòu)造器* 所以在外部是new不到他的* 然后我們直接getInstance* * 這樣簡單一個單線程獲取的單例呢就完成了* 只有使用它的時候才初始化* 如果不使用就初始化LazySingleton對象* 那main本身是一個線程* 現(xiàn)在我們在這個線程中再創(chuàng)建兩個線程* 去獲取現(xiàn)在這種寫法的單例的時候* 會碰到什么問題呢* 我們一起來看一下* 首先我們寫一下線程的類* * * */
// LazySingleton o1 = LazySingleton.getInstance();
// System.out.println("Program end.....");
//
// Field flag = o1.getClass().getDeclaredField("flag");
// flag.setAccessible(true);
// flag.set(o1,true);
//
//
// LazySingleton o2 = (LazySingleton) c.newInstance();
//
// System.out.println(o1);
// System.out.println(o2);
// System.out.println(o1==o2);
// }}
package com.learn.design.pattern.creational.singleton;import java.io.IOException;
import java.lang.reflect.InvocationTargetException;/*** 本身Main是一個主線程* 從上至下而執(zhí)行* 那執(zhí)行到t1.start();的時候呢* 就開啟了兩個線程* 一會我們在執(zhí)行的時候其實(shí)是三個線程在執(zhí)行* 主線程還有t1和t2* 那斷點(diǎn)呢我們也打上了* 現(xiàn)在運(yùn)行debug* * 但是剛剛通過debug我們已經(jīng)知道了* 他們返回同一個對象* 是因?yàn)樽詈蟮木€程重新賦值了* 并且在重新賦值之后* 兩個線程才進(jìn)行return的* 所以我們在console里面看到的是同一個對象* 把我們再debug操作一下* 讓他們返回不同的對象* * * @author Leon.Sun**/
public class Test {public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// LazySingleton lazySingleton = LazySingleton.getInstance();// System.out.println("main thread"+ThreadLocalInstance.getInstance());
// System.out.println("main thread"+ThreadLocalInstance.getInstance());
// System.out.println("main thread"+ThreadLocalInstance.getInstance());
// System.out.println("main thread"+ThreadLocalInstance.getInstance());
// System.out.println("main thread"+ThreadLocalInstance.getInstance());
// System.out.println("main thread"+ThreadLocalInstance.getInstance());/*** new一個t1線程* */Thread t1 = new Thread(new T());/*** 再new一個t2線程* */Thread t2 = new Thread(new T());/*** 我們可以看到Thread1和Thread0拿到的是同一個對象* 那這個呢是run的情況下* 如果我們debug進(jìn)行干預(yù)的話* * */t1.start();t2.start();/*** 現(xiàn)在這個線程是主線程的* 我們關(guān)心的是Main Thread0 Thread1* 那現(xiàn)在這個三個線程的狀態(tài)都是Running* 那我們現(xiàn)在通過Frame切換到Thread0上* 可以看到Thread0調(diào)用getInstance方法了* * */System.out.println("program end");// HungrySingleton instance = HungrySingleton.getInstance();
// EnumInstance instance = EnumInstance.getInstance();
// instance.setData(new Object());
//
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
// oos.writeObject(instance);
//
// File file = new File("singleton_file");
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//HungrySingleton newInstance = (HungrySingleton) ois.readObject();
// EnumInstance newInstance = (EnumInstance) ois.readObject();
//
// System.out.println(instance.getData());
// System.out.println(newInstance.getData());
// System.out.println(instance.getData() == newInstance.getData());// Class objectClass = HungrySingleton.class;
// Class objectClass = StaticInnerClassSingleton.class;// Class objectClass = LazySingleton.class;
// Class objectClass = EnumInstance.class;// Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
//
// constructor.setAccessible(true);
// EnumInstance instance = (EnumInstance) constructor.newInstance("Geely",666);//
// LazySingleton newInstance = (LazySingleton) constructor.newInstance();
// LazySingleton instance = LazySingleton.getInstance();// StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
// StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();// HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
// HungrySingleton instance = HungrySingleton.getInstance();// System.out.println(instance);
// System.out.println(newInstance);
// System.out.println(instance == newInstance);// EnumInstance instance = EnumInstance.getInstance();
// instance.printTest();}
}
?
總結(jié)
以上是生活随笔為你收集整理的单列设计模式 懒汉式及多线程debug的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 单例模式讲解
- 下一篇: DoubleCheck双重检查实战及原理