<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>

          教妹學(xué)Java:并發(fā)編程之 volatile

          共 13415字,需瀏覽 27分鐘

           ·

          2022-06-21 02:54

          二哥編程知識(shí)星球 (戳鏈接加入)正式上線了,來(lái)和 260 多名 小伙伴一起打怪升級(jí)吧!這是一個(gè) Java 學(xué)習(xí)指南 + 編程實(shí)戰(zhàn)的私密圈子,你可以向二哥提問(wèn)、幫你制定學(xué)習(xí)計(jì)劃、跟著二哥一起做實(shí)戰(zhàn)項(xiàng)目,沖沖沖。

          Java 程序員進(jìn)階之路網(wǎng)址:https://tobebetterjavaer.com

          “三妹啊,這節(jié)我們來(lái)學(xué)習(xí) Java 并發(fā)編程中的 volatile 關(guān)鍵字,以及容易遇到的坑。”看著三妹好學(xué)的樣子,我倍感欣慰。

          “好呀,哥。”三妹愉快的答應(yīng)了。

          volatile 變量的特性

          volatile 可以保證可見(jiàn)性,但不保證原子性:

          • 當(dāng)寫(xiě)一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去;
          • 這個(gè)寫(xiě)操作會(huì)導(dǎo)致其他線程中的 volatile 變量緩存無(wú)效。

          volatile 禁止指令重排規(guī)則

          我們回顧一下,重排序需要遵守一定規(guī)則:

          • 重排序操作不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。比如:a=1;b=a; 這個(gè)指令序列,由于第二個(gè)操作依賴于第一個(gè)操作,所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會(huì)被重排序。
          • 重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變。比如:a=1;b=2;c=a+b 這三個(gè)操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關(guān)系, 所以可能會(huì)發(fā)生重排序,但是 c=a+b 這個(gè)操作是不會(huì)被重排序的,因?yàn)樾枰WC最終的結(jié)果一定是 c=a+b=3。

          使用 volatile 關(guān)鍵字修飾共享變量可以禁止這種重排序。若用 volatile 修飾共享變量,在編譯時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序,volatile 禁止指令重排序也有一些規(guī)則:

          • 當(dāng)程序執(zhí)行到 volatile 變量的讀操作或者寫(xiě)操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見(jiàn);在其后面的操作肯定還沒(méi)有進(jìn)行;
          • 在進(jìn)行指令優(yōu)化時(shí),不能將對(duì) volatile 變量訪問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把 volatile 變量后面的語(yǔ)句放到其前面執(zhí)行。

          “二哥,能不能通俗地講講啊?”

          “也就是說(shuō),執(zhí)行到 volatile 變量時(shí),其前面的所有語(yǔ)句都執(zhí)行完,后面所有語(yǔ)句都未執(zhí)行。且前面語(yǔ)句的結(jié)果對(duì) volatile 變量及其后面語(yǔ)句可見(jiàn)。”我瞅了了三妹一眼說(shuō)。

          volatile 禁止指令重排分析

          先看下面未使用 volatile 的代碼:

          class ReorderExample {
            int a = 0;
            boolean flag = false;
            public void writer() {
                a = 1;                   //1
                flag = true;             //2
            }
            Public void reader() {
                if (flag) {                //3
                    int i =  a * a;        //4
                    System.out.println(i);
                }
            }
          }

          因?yàn)橹嘏判蛴绊懀宰罱K的輸出可能是 0,具體分析請(qǐng)參考上一篇,如果引入 volatile,我們?cè)倏匆幌麓a:

          class ReorderExample {
            int a = 0;
            boolean volatile flag = false;
            public void writer() {
                a = 1;                   //1
                flag = true;             //2
            }
            Public void reader() {
                if (flag) {                //3
                    int i =  a * a;        //4
                    System.out.println(i);
                }
            }
          }

          這個(gè)時(shí)候,volatile 禁止指令重排序也有一些規(guī)則,這個(gè)過(guò)程建立的 happens before 關(guān)系可以分為兩類:

          1. 根據(jù)程序次序規(guī)則,1 happens before 2; 3 happens before 4。
          2. 根據(jù) volatile 規(guī)則,2 happens before 3。
          3. 根據(jù) happens before 的傳遞性規(guī)則,1 happens before 4。

          上述 happens before 關(guān)系的圖形化表現(xiàn)形式如下:

          在上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn),代表了一個(gè) happens before 關(guān)系:

          • 黑色箭頭表示程序順序規(guī)則;
          • 橙色箭頭表示 volatile 規(guī)則;
          • 藍(lán)色箭頭表示組合這些規(guī)則后提供的 happens before 保證。

          這里 A 線程寫(xiě)一個(gè) volatile 變量后,B 線程讀同一個(gè) volatile 變量。A 線程在寫(xiě) volatile 變量之前所有可見(jiàn)的共享變量,在 B 線程讀同一個(gè) volatile 變量后,將立即變得對(duì) B 線程可見(jiàn)。

          volatile 不適用場(chǎng)景

          volatile 不適合復(fù)合操作

          下面是變量自加的示例:

          public class volatileTest {
              public volatile int inc = 0;
              public void increase() {
                  inc++;
              }
              public static void main(String[] args) {
                  final volatileTest test = new volatileTest();
                  for(int i=0;i<10;i++){
                      new Thread(){
                          public void run() {
                              for(int j=0;j<1000;j++)
                                  test.increase();
                          };
                      }.start();
                  }
                  while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  System.out.println("inc output:" + test.inc);
              }
          }

          測(cè)試輸出:

          inc output:8182

          “為什么呀?二哥?”三妹疑惑地問(wèn)。

          “因?yàn)?inc++不是一個(gè)原子性操作,由讀取、加、賦值 3 步組成,所以結(jié)果并不能達(dá)到 10000。”我耐心地回答。

          “哦,你這樣說(shuō)我就理解了。”三妹點(diǎn)點(diǎn)頭。

          解決方法

          采用 synchronized:

          public class volatileTest1 {
              public int inc = 0;
              public synchronized void increase() {
                  inc++;
              }
              public static void main(String[] args) {
                  final volatileTest1 test = new volatileTest1();
                  for(int i=0;i<10;i++){
                      new Thread(){
                          public void run() {
                              for(int j=0;j<1000;j++)
                                  test.increase();
                          };
                      }.start();
                  }
                  while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  System.out.println("add synchronized, inc output:" + test.inc);
              }
          }

          采用 Lock:

          public class volatileTest2 {
              public int inc = 0;
              Lock lock = new ReentrantLock();
              public void increase() {
                  lock.lock();
                  inc++;
                  lock.unlock();
              }
              public static void main(String[] args) {
                  final volatileTest2 test = new volatileTest2();
                  for(int i=0;i<10;i++){
                      new Thread(){
                          public void run() {
                              for(int j=0;j<1000;j++)
                                  test.increase();
                          };
                      }.start();
                  }
                  while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  System.out.println("add lock, inc output:" + test.inc);
              }
          }

          采用 AtomicInteger:

          public class volatileTest3 {
              public AtomicInteger inc = new AtomicInteger();
              public void increase() {
                  inc.getAndIncrement();
              }
              public static void main(String[] args) {
                  final volatileTest3 test = new volatileTest3();
                  for(int i=0;i<10;i++){
                      new Thread(){
                          public void run() {
                              for(int j=0;j<100;j++)
                                  test.increase();
                          };
                      }.start();
                  }
                  while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  System.out.println("add AtomicInteger, inc output:" + test.inc);
              }
          }

          三者輸出都是 1000,如下:

          add synchronized, inc output:1000
          add lock, inc output:1000
          add AtomicInteger, inc output:1000

          單例模式的雙重鎖要加volatile

          先看一下單例代碼:

          public class penguin {
              private static volatile penguin m_penguin = null;
              // 避免通過(guò)new初始化對(duì)象
              private void penguin() {}
              public void beating() {
                  System.out.println("打豆豆");
              };
              public static penguin getInstance() {      //1
                  if (null == m_penguin) {               //2
                      synchronized(penguin.class) {      //3
                          if (null == m_penguin) {       //4
                              m_penguin = new penguin(); //5
                          }
                      }
                  }
                  return m_penguin;                      //6
              }
          }

          在并發(fā)情況下,如果沒(méi)有 volatile 關(guān)鍵字,在第 5 行會(huì)出現(xiàn)問(wèn)題。instance = new TestInstance();可以分解為 3 行偽代碼:

          a. memory = allocate() //分配內(nèi)存
          b. ctorInstanc(memory) //初始化對(duì)象
          c. instance = memory   //設(shè)置instance指向剛分配的地址

          上面的代碼在編譯運(yùn)行時(shí),可能會(huì)出現(xiàn)重排序從 a-b-c 排序?yàn)?a-c-b。在多線程的情況下會(huì)出現(xiàn)以下問(wèn)題。

          當(dāng)線程 A 在執(zhí)行第 5 行代碼時(shí),B 線程進(jìn)來(lái)執(zhí)行到第 2 行代碼。假設(shè)此時(shí) A 執(zhí)行的過(guò)程中發(fā)生了指令重排序,即先執(zhí)行了 a 和 c,沒(méi)有執(zhí)行 b。那么由于 A 線程執(zhí)行了 c 導(dǎo)致 instance 指向了一段地址,所以 B 線程判斷 instance 不為 null,會(huì)直接跳到第 6 行并返回一個(gè)未初始化的對(duì)象。

          總結(jié)

          “好了,三妹,我們來(lái)總結(jié)一下。”我舒了一口氣說(shuō)。

          volatile 可以保證線程可見(jiàn)性且提供了一定的有序性,但是無(wú)法保證原子性。在 JVM 底層 volatile 是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的。

          觀察加入 volatile 關(guān)鍵字和沒(méi)有加入 volatile 關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入 volatile 關(guān)鍵字時(shí),會(huì)多出一個(gè) lock 前綴指令,lock 前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也稱內(nèi)存柵欄),內(nèi)存屏障會(huì)提供 3 個(gè)功能:

          • 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
          • 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫(xiě)入主存;
          • 如果是寫(xiě)操作,它會(huì)導(dǎo)致其他 CPU 中對(duì)應(yīng)的緩存行無(wú)效。

          最后,我們學(xué)習(xí)了 volatile 不適用的場(chǎng)景,以及解決的方法,并解釋了單例模式為何需要使用 volatile。


          沒(méi)有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧?kù)o的港灣,我是不系之舟。歡迎大家多多點(diǎn)贊,更多精彩內(nèi)容,也請(qǐng)關(guān)注二哥新成立的三劍客之一“樓仔”,我們也會(huì)做一些令大家驚喜的事情出來(lái),敬請(qǐng)期待!

          推薦閱讀

          瀏覽 41
          點(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>
                  色五月婷婷俺来也 | 另类中文字幕 | www.最全三级在线 | 影音先锋男人网站 | 色情一级A片成人片 |