<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          美團(tuán)面試題:DCL單例模式需不需要volatile?

          共 4215字,需瀏覽 9分鐘

           ·

          2021-06-01 16:37

          最近有粉絲收到美團(tuán)的面試,去試了一試,結(jié)果沒(méi)有過(guò)。然后在群里分享面試經(jīng)歷,其中有一個(gè)面試題《DCL 單例模式到底需不需要 volatile?》引起了大家的爭(zhēng)議,今天我們一起來(lái)討論討論這個(gè)面試題!

          既然講到單例,我們先來(lái)看幾個(gè)經(jīng)典的單例實(shí)現(xiàn)。

          public class Xttblog {
          private static Xttblog instance = new Xttblog();
          private Xttblog() {}
          public static Xttblog getInstance() {
          return instance;
          }
          }

          上面的代碼是一種最經(jīng)典,最簡(jiǎn)單的單例模式。這種寫法無(wú)論在單線程還是多線程環(huán)境下都不會(huì)出現(xiàn)任何安全性問(wèn)題。但是,這種實(shí)現(xiàn)方式有一個(gè)缺點(diǎn):無(wú)論這個(gè)單例是否被使用,都會(huì)在內(nèi)存中創(chuàng)建一個(gè)這樣的單例。所以出現(xiàn)了后續(xù)的懶加載實(shí)現(xiàn)方式。

          懶加載單例模式

          public class Xttblog {
          private static Xttblog instance;
          private Xttblog() {}
          public static Xttblog getInstance() {
          if (instance == null) { // 1
          instance = new Xttblog();
          }
          return instance;
          }
          }

          懶加載即使用單例的時(shí)候才初始化。但是,這種實(shí)現(xiàn)方式有一個(gè)明顯的缺點(diǎn):當(dāng)在多線程環(huán)境下,多個(gè)線程同時(shí)運(yùn)行到代碼 1 處時(shí),instance 為 null,這幾個(gè)線程都會(huì)創(chuàng)建自己的單例,而不是使用的同一個(gè)單例對(duì)象。如果給 getInstance()方法加上同步關(guān)鍵字呢?

          懶加載加鎖單例模式

          public class Xttblog {
          private static Xttblog instance;
          private Xttblog() {}
          public static synchronized Xttblog getInstance() {
          if (instance == null) {
          instance = new Xttblog();
          }
          return instance;
          }
          }

          方法加鎖的實(shí)現(xiàn)方式自然能保證多線程環(huán)境下的安全性,但是方法加鎖的方式會(huì)嚴(yán)重影響性能。接下來(lái)考慮細(xì)粒度的加鎖方式——代碼塊加鎖。

          public class Xttblog {
          private static Xttblog instance;
          private Xttblog() {}
          public static Xttblog getInstance() {
          if (instance == null) { // 1
          synchronized (Xttblog.class) {
          instance = new Xttblog();
          }
          }
          return instance;
          }
          }

          但是這種細(xì)粒度的加鎖方式并不能保證多線程環(huán)境下的安全性。舉例說(shuō)明:A,B 兩個(gè)線程同時(shí)運(yùn)行到代碼 1 處,接下來(lái)兩個(gè)線程會(huì)競(jìng)爭(zhēng) Xttblog 類鎖。假如 A 線程獲得了鎖,A 線程繼續(xù)執(zhí)行,直到 A 線程創(chuàng)建一個(gè) Xttblog 實(shí)例對(duì)象,A 線程釋放鎖。這時(shí) B 線程獲取到鎖,依然是繼續(xù)執(zhí)行,此時(shí) B 線程仍然會(huì)創(chuàng)建一個(gè) Xttblog 實(shí)例對(duì)象。A,B 兩個(gè)線程就創(chuàng)建了兩個(gè)不同的 Xttblog 實(shí)例對(duì)象。接下來(lái)繼續(xù)改進(jìn),如果在同步代碼塊中再加一層 check,check instance 是否為null —— 雙重驗(yàn)證(DCL —— Double Check Lock)

          DCL(Double Check Lock)單例模式

          public class Xttblog {
          private static Xttblog instance;
          private Xttblog() {}
          public static Xttblog getInstance() {
          if (instance == null) { // 1
          synchronized (Xttblog.class) {
          if (instance == null) { // 2
          instance = new Xttblog();
          }
          }
          }
          return instance;
          }
          }

          根據(jù)上面的解析,A 線程執(zhí)行完畢釋放鎖后,B 線程獲取到鎖時(shí),再次檢查 instance 是否為 Null。由于 synchronized 可以保證線程可見(jiàn)性,所以 A 線程中對(duì) instance 賦值后會(huì)將值刷新到主存中去,并且導(dǎo)致所有線程中關(guān)于 instance 的線程本地緩存值都會(huì)失效。B 線程運(yùn)行到代碼 2 處時(shí),B 線程的 instance 在線程本地緩存中的值已經(jīng)失效,所以會(huì)重新去主存中去拿,這時(shí) B 線程在代碼 2 處的返回值為 false。所以 B 線程不再創(chuàng)建新的對(duì)象,而直接返回。雙重驗(yàn)證總歸可以了吧?答案還是 No。這是很多人認(rèn)為理所當(dāng)然的,感覺(jué)經(jīng)得起推敲。但是,如果大家了解對(duì)象的創(chuàng)建過(guò)程,并且知道在多線程環(huán)境下,線程有可能會(huì)使用一個(gè)未完全初始化的對(duì)象,就會(huì)明白為什么這種方案還是不行。接下來(lái)大概描述一下對(duì)象的創(chuàng)建過(guò)程,以及什么是未完全初始化的對(duì)象。這也是美團(tuán)面試官問(wèn)到這道題的真實(shí)意圖。

          Java 對(duì)象的創(chuàng)建過(guò)程

          public class TestNewObj {
          public static void main(String[] args) {
          T t = new T();
          }

          }
          class T {
          int m = 10;
          }

          執(zhí)行 javac ./TestNewObj.java 得到 class 文件。

          執(zhí)行 javap -c ./TestNewObj.class 查看字節(jié)碼指令,本例如下:

          查看字節(jié)碼指令

          創(chuàng)建對(duì)象的指令就在紅色圈中。

          對(duì)象的創(chuàng)建過(guò)程用到的指令

          對(duì)象的創(chuàng)建過(guò)程new Object(),根據(jù)如上 Java 代碼進(jìn)行如下描述:

          1. 申請(qǐng)內(nèi)存,此時(shí) m 的值是默認(rèn)值 0

          2. 調(diào)用構(gòu)造方法,此時(shí) m 值為 8

          3. 建立關(guān)聯(lián),t 指向申請(qǐng)的內(nèi)存

          這 3 個(gè)步驟分別對(duì)應(yīng)上圖指令中的 0, 4, 7。

          public class Xttblog {
          private static Xttblog instance;
          private Xttblog() {}
          public static Xttblog getInstance() {
          if (instance == null) { // 1
          synchronized (Xttblog.class) {
          if (instance == null) { // 2
          instance = new Xttblog();
          }
          }
          }
          return instance;
          }
          }

          然后回想一下 DCL 單例中,如果存在這樣一種情形:

          兩個(gè)線程 A、B,線程 A 獲取到鎖,當(dāng)線程 A 創(chuàng)建對(duì)象 T,指令執(zhí)行到指令 0 時(shí)(此時(shí) m = 0),發(fā)生了指令重排序,指令 4 和指令 7 位置互換,即由之前的執(zhí)行順序 4->7 變成了 7->4。此時(shí)發(fā)生指令重排時(shí),會(huì)將一個(gè)半初始化的對(duì)象與 t 建立關(guān)聯(lián),并且此時(shí)的 m=0。所以當(dāng)線程 B 到達(dá)代碼 1 處時(shí)判斷 instance 是否為 null,此時(shí) instance 已經(jīng)不為 null 了,就直接用了一個(gè)半初始化狀態(tài)的對(duì)象,m = 0,這就是其安全性問(wèn)題所在。要問(wèn)這個(gè)情況如何驗(yàn)證,抱歉,以普通項(xiàng)目超低并發(fā)的水平做不到,但是相信如果有阿里那樣的并發(fā)量,一定會(huì)出現(xiàn)。

          Java對(duì)象創(chuàng)建過(guò)程

          根本原因在于,Java 對(duì)象的創(chuàng)建不是原子性操作,所以有指令重排序的可能。為了禁止指令重排序,所以要引入 volatile。終于點(diǎn)題了—— DCL 單例模式需不需要volatile?為什么?

          答案是肯定的,需要 volatile。

          public class Xttblog {
          private static volatile Xttblog instance;
          private Xttblog() {}
          public static Xttblog getInstance() {
          if (instance == null) { // 1
          synchronized (Xttblog.class) {
          if (instance == null) { // 2
          instance = new Xttblog();
          }
          }
          }
          return instance;
          }
          }

          所以 volatile 在 DCL 單例中不是使用它的線程可見(jiàn)性,而是禁止指令重排序

          結(jié)論

          對(duì)象的創(chuàng)建不是原子性操作,所以有指令重排序的可能。為了禁止指令重排序,所以要引入 volatile。

          瀏覽 31
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人精品无码区免费福利 | 一道本高清无码视频 | 乱伦视频导航 | 蜜芽欧洲无码精品 | xjgggyxgs.com高价收liang,请涟系@qdd2000 |