1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        因?yàn)槲艺f:volatile 是輕量級(jí)的 synchronized,面試官讓我回去等通知!

        共 3078字,需瀏覽 7分鐘

         ·

        2020-03-31 23:26

        volatile 是并發(fā)編程的重要組成部分,也是面試常被問到的問題之一。不要向小強(qiáng)那樣,因?yàn)橐痪洌簐olatile 是輕量級(jí)的 synchronized,而與期望已久的大廠失之交臂。
        volatile 有兩大特性:保證內(nèi)存的可見性和禁止指令重排序。那什么是可見性和指令重排呢?接下來我們一起來看。


        內(nèi)存可見性

        要了解內(nèi)存可見性先要從 Java 內(nèi)存模型(JMM)說起,在 Java 中所有的共享變量都在主內(nèi)存中,每個(gè)線程都有自己的工作內(nèi)存,為了提高線程的運(yùn)行速度,每個(gè)線程的工作內(nèi)存都會(huì)把主內(nèi)存中的共享變量拷貝一份進(jìn)行緩存,以此來提高運(yùn)行效率
        內(nèi)存布局如下圖所示:

        15b05d8a39c5f2d40e82936c5e635fc5.webp

        但這樣就會(huì)產(chǎn)生一個(gè)新的問題,如果某個(gè)線程修改了共享變量的值,其他線程不知道此值被修改了,就會(huì)發(fā)生兩個(gè)線程值不一致的情況,我們用代碼來演示一下這個(gè)問題。
        public class VolatileExample {
        // 可見性參數(shù)
        private static boolean flag = false;

        public static void main(String[] args) {
        new Thread(() -> {
        try {
        // 暫停 0.5s 執(zhí)行
        Thread.sleep(500);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        flag = true;
        System.out.println("flag 被修改成 true");
        }).start();

        // 一直循環(huán)檢測(cè) flag=true
        while (true) {
        if (flag) {
        System.out.println("檢測(cè)到 flag 變?yōu)?true");
        break;
        }
        }
        }
        }
        以上程序的執(zhí)行結(jié)果如下:
        flag 被修改成 true
        我們會(huì)發(fā)現(xiàn)永遠(yuǎn)等不到 檢測(cè)到 flag 變?yōu)?true?的結(jié)果,這是因?yàn)榉侵骶€程更改了 flag=true,但主線程一直不知道此值發(fā)生了改變,這就是內(nèi)存不可見的問題。內(nèi)存的可見性是指線程修改了變量的值之后,其他線程能立即知道此值發(fā)生了改變。我們可以使用 volatile 來修飾 flag,就可以保證內(nèi)存的可見性,代碼如下:
        public class VolatileExample {
        // 可見性參數(shù)
        private static volatile boolean flag = false;

        public static void main(String[] args) {
        new Thread(() -> {
        try {
        // 暫停 0.5s 執(zhí)行
        Thread.sleep(500);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        flag = true;
        System.out.println("flag 被修改成 true");
        }).start();

        // 一直循環(huán)檢測(cè) flag=true
        while (true) {
        if (flag) {
        System.out.println("檢測(cè)到 flag 變?yōu)?true");
        break;
        }
        }
        }
        }
        以上程序的執(zhí)行結(jié)果如下:
        檢測(cè)到 flag 變?yōu)?true flag 被修改成 true


        指令重排

        指令重排是指在執(zhí)行程序時(shí),編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序,已到達(dá)提高程序性能的目的。比如小強(qiáng)要去圖書館還上次借的書,隨便再借一本新書,而此時(shí)室友小王也想讓小強(qiáng)幫他還一本書,未發(fā)生指令重排的做法是,小強(qiáng)先把自己的事情辦完,再去辦室友的事,這樣顯然比較浪費(fèi)時(shí)間,還有一種做法是,他先把自己的書和小王的書一起還掉,再給自己借一本新書,這就是指令重排的意義。但指令重排不能保證指令執(zhí)行的順序,這就會(huì)造成新的問題,如下代碼所示:
        public class VolatileExample {
        // 指令重排參數(shù)
        private static int a = 0, b = 0;
        private static int x = 0, y = 0;

        public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
        Thread t1 = new Thread(() -> {
        // 有可能發(fā)生指令重排,先 x=b 再 a=1
        a = 1;
        x = b;
        });
        Thread t2 = new Thread(() -> {
        // 有可能發(fā)生指令重排,先 y=a 再 b=1
        b = 1;
        y = a;
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("第 " + i + "次,x=" + x + " | y=" + y);
        if (x == 0 && y == 0) {
        // 發(fā)生了指令重排
        break;
        }
        // 初始化變量
        a = 0;
        b = 0;
        x = 0;
        y = 0;
        }
        }
        }
        以上程序執(zhí)行結(jié)果如下所示:

        092fff8a0955082b3310ffe6b3327b3a.webp

        可以看出執(zhí)行到 48526 次時(shí)發(fā)生了指令重排,y 就變成了非正確值 0,顯然這不是我們想要的結(jié)果,這個(gè)時(shí)候就可以使用 volatile 來禁止指令重排。以上我們通過代碼的方式演示了指令重排和內(nèi)存可見性的問題,接下來我們用代碼來演示一下 volatile 同步方式的問題。


        volatile?非同步方式

        首先,我們使用 volatile 修飾一個(gè)整數(shù)變量,再啟動(dòng)兩個(gè)線程分別執(zhí)行同樣次數(shù)的 ++ 和 --?操作,最后發(fā)現(xiàn)執(zhí)行的結(jié)果竟然不是 0,代碼如下:
        public class VolatileExample {
        public static volatile int count = 0; // 計(jì)數(shù)器
        public static final int size = 100000; // 循環(huán)測(cè)試次數(shù)

        public static void main(String[] args) {
        // ++ 方式
        Thread thread = new Thread(() -> {
        for (int i = 1; i <= size; i++) {
        count++;
        }
        });
        thread.start();
        // -- 方式
        for (int i = 1; i <= size; i++) {
        count--;
        }
        // 等所有線程執(zhí)行完成
        while (thread.isAlive()) {}
        System.out.println(count); // 打印結(jié)果
        }
        }
        以上程序執(zhí)行結(jié)果如下:
        1065
        可以看出,執(zhí)行結(jié)果并不是我們期望的結(jié)果 0,我們把以上代碼使用 synchronized 改造一下:
        public class VolatileExample {
        public static int count = 0; // 計(jì)數(shù)器
        public static final int size = 100000; // 循環(huán)測(cè)試次數(shù)

        public static void main(String[] args) {
        // ++ 方式
        Thread thread = new Thread(() -> {
        for (int i = 1; i <= size; i++) {
        synchronized (VolatileExample.class) {
        count++;
        }
        }
        });
        thread.start();
        // -- 方式
        for (int i = 1; i <= size; i++) {
        synchronized (VolatileExample.class) {
        count--;
        }
        }
        // 等所有線程執(zhí)行完成
        while (thread.isAlive()) {}
        System.out.println(count); // 打印結(jié)果
        }
        }
        這次執(zhí)行的結(jié)果變成了我們期望的值 0。這說明?volatile?只是輕量級(jí)的線程可見方式,并不是輕量級(jí)的同步方式,所以并不能說?volatile?是輕量級(jí)的?synchronized,終于知道為什么面試官讓我回去等通知了。


        volatile 使用場(chǎng)景

        既然 volatile 只能保證線程操作的可見方式,那它有什么用呢?volatile?在多讀多寫的情況下雖然一定會(huì)有問題,但如果是一寫多讀的話使用?volatile?就不會(huì)有任何問題。volatile 一寫多讀的經(jīng)典使用示例就是 CopyOnWriteArrayList,CopyOnWriteArrayList 在操作的時(shí)候會(huì)把全部數(shù)據(jù)復(fù)制出來對(duì)寫操作加鎖,修改完之后再使用 setArray 方法把此數(shù)組賦值為更新后的值,使用 volatile 可以使讀線程很快的告知到數(shù)組被修改,不會(huì)進(jìn)行指令重排,操作完成后就可以對(duì)其他線程可見了,核心源碼如下:
        public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
        {

        private transient volatile Object[] array;

        final void setArray(Object[] a) {
        array = a;
        }
        //...... 忽略其他代碼
        }


        總結(jié)

        本文我們通過代碼的方式演示了 volatile 的兩大特性,內(nèi)存可見性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并非輕量級(jí)的同步方式,以及 volatile 一寫多讀的經(jīng)典使用案例 CopyOnWriteArrayList。

        有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

        歡迎大家關(guān)注Java之道公眾號(hào)


        好文章,我在看??

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            麻豆熟妇乱妇熟色A片在线看 | 国产精久久久久久 | 中文字幕第二页在线观看 | 久久大奶| 国产天堂在线观看 | 熟女人妻在线观看 | 日本高清不卡视频 | 樱桃一区二区三区 | 影音先锋男人网站 | 床戏麻豆|