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

          Java中的volatile關(guān)鍵字最全總結(jié)

          共 7895字,需瀏覽 16分鐘

           ·

          2021-11-14 17:05

          點擊關(guān)注公眾號,Java干貨及時送達(dá)

          作者?|?汪偉俊

          ?|?公眾號:Java技術(shù)迷(JavaFans1024)


          粉絲福利:小編會從今天留言的小伙伴中隨機(jī)抽贈送8.88元現(xiàn)金紅包。娛樂抽獎,大家隨緣積極參與啦,給生活一點小幸運~感謝大家的支持

          變量的不可見問題

          來看一個簡單的案例:

          public class VolatileDemo {    public static void main(String[] args) {        MyThread myThread = new MyThread();        myThread.start();
          while (true) { if(myThread.isTag()){ System.out.println("----------"); } } }}
          @Dataclass MyThread extends Thread { private boolean tag; @Override public void run() { tag = true; System.out.println("子線程中tag為:" + tag); }}

          這段程序應(yīng)該非常好理解,當(dāng)子線程被創(chuàng)建并調(diào)用時,子線程會執(zhí)行run()方法將tag的屬性值修改為true,修改完成后main方法中的while循環(huán)條件就成立了,所以程序的運行結(jié)果應(yīng)該是:

          子線程中tag為:true----------

          但事實上,運行結(jié)果是這樣的:


          image.png

          那么很明顯,main方法并沒有得到子線程修改后的tag值,這一現(xiàn)象就是多線程下變量的不可見問題。


          那么main方法為何得不到子線程修改后的tag值呢?我們需要來了解一下JMM(Java內(nèi)存模型)。在JMM中,所有的共享變量都會被保存到主存中,共享變量指的是實例變量和成員變量,局部變量不屬于共享變量,他是線程私有的;當(dāng)啟動某個線程時,會開辟一個獨立的內(nèi)存空間提供線程使用,并從主存中拷貝一個共享變量的副本存入線程的內(nèi)存里,線程只能對該變量的副本進(jìn)行讀寫操作,不能直接操作主存中的共享變量,不同的線程之間也無法相互訪問獨立空間中的變量,而是需要通過主存進(jìn)行數(shù)據(jù)的傳遞,如下圖所示:

          image.png

          由此,我們可以分析剛才的程序:



          image.png

          首先主存中有一個共享變量tag,值為false,當(dāng)子線程啟動時,便會拷貝一份tag的副本存放于子線程的內(nèi)存空間中,然而在子線程將tag值修改并寫回主存之前,main線程也從主存中拷貝了一個tag的副本,所以此時main線程中tag的值仍然為false,這導(dǎo)致main線程中的if條件不成立,又因為while(true)底層的一些原因,使得它的執(zhí)行效率非常地高,使得main線程無法再去主存中重新讀取tag的值,即:取的一直都是main線程中的變量副本,這也就解釋了為什么會出現(xiàn)變量不可見的問題了。


          解決變量不可見的問題

          既然出現(xiàn)了這一問題,那么該如何去解決它呢?

          while (true) {    synchronized (myThread){        if(myThread.isTag()){            System.out.println("----------");        }    }}

          只需使用同步代碼塊將使用到共享變量的代碼包裹起來即可,此時代碼的執(zhí)行流程如下:

          1.main線程獲取到鎖2.清空線程私有的內(nèi)存空間3.從主存中拷貝一份共享變量的副本到私有內(nèi)存4.對變量副本進(jìn)行操作5.將修改后的變量副本的值重新放回主存6.main線程釋放鎖

          由于main線程在使用tag時需要清空一次內(nèi)存,并重新獲取,這樣就能夠保證main線程在讀取tag值的時候一定是最新的,而synchronized關(guān)鍵字的性能是比較差的,對于這種問題,使用?volatile?關(guān)鍵字將會顯得更加優(yōu)雅,我們只需要使用volatile關(guān)鍵字修飾共享變量即可:

          private volatile boolean tag;

          那么它的原理又是什么呢?首先子線程和main線程仍然會從主存中復(fù)制得到共享變量的副本,當(dāng)子線程修改了共享變量但還未寫入主存時,main線程獲取到了共享變量的舊值,而由于共享變量被volatile修飾,所以當(dāng)子線程將值寫回主存時,會使其它線程的共享變量副本失效,失效后其它線程就會重新去主存獲取一次值,這樣也能夠獲取到最新的數(shù)據(jù)。

          volatile能夠保證不同線程對共享變量的操作可見性,當(dāng)某個線程修改了共享變量的值時,其它線程便能夠立即看到最新的值。

          vloatile關(guān)鍵字還有一個特殊的性質(zhì),就是可以禁止指令的重排序,編譯器為了提高程序的運行效率,它往往會對執(zhí)行指令進(jìn)行一個重排序,前提是不會影響到程序最終的運行結(jié)果,比如:

          int a = 1;int b = 2;a = 3;

          在這段程序中,按照從上到下的順序,首先需要將a的值保存為1,再將b的值保存為2,最后重新將a的值保存為3,但為了提高效率,編譯器可能會重新設(shè)置代碼的執(zhí)行順序:

          int a = 1;a = 3;int b = 2;

          此時只需保存a的值為3,再保存b的值為2,這樣就省略了一個步驟,提高了性能,需要注意的是指令重排序不能影響到程序最終的運行結(jié)果,所以語句?a = 3?肯定不會在?int a = 1?之前被執(zhí)行。

          來看一個例子:

          public class VolatileDemo {
          private static int a, b, i, j = 0;
          public static void main(String[] args) throws InterruptedException { while (true) { a = 0; b = 0; i = 0; j = 0; Thread t1 = new Thread(() -> { a = 1; i = b; }); Thread t2 = new Thread(() -> { b = 1; j = a; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("i = " + i + ", j = " + j); } }}

          該程序中共有2個子線程,分別會去修改四個變量的值,然后輸出被子線程修改后的變量值,這里調(diào)用join()方法是為了讓主線程等待子線程執(zhí)行完畢后才去輸出變量值。我們可以猜測一下程序的運行結(jié)果,若是線程t1先執(zhí)行,線程t2后執(zhí)行,則i的值為0,j的值為1;若是線程t2先執(zhí)行,線程t1后執(zhí)行,則i的值為1,j的值為0;若是線程t1在執(zhí)行過程中,t2也得到了執(zhí)行,則i的值為1,j的值也為1,然而在運行程序之后,卻得到了第4種結(jié)果:

          ......i = 0, j = 1i = 1, j = 1i = 0, j = 1i = 0, j = 0

          i和j的值竟然均為0?這是為什么呢?原來,這是指令重排序?qū)е碌?,編譯器為了優(yōu)化程序,很可能會將指令執(zhí)行順序重新排序,比如這樣:

          Thread t1 = new Thread(() -> {    i = b;    a = 1;});Thread t2 = new Thread(() -> {    j = a;    b = 1;});

          此時當(dāng)線程t1在執(zhí)行過程中,線程t2被執(zhí)行,那么i和j的值就都為0了,這顯然是違背我們正常思維的,為了防止這種情況的發(fā)生,可以使用?volatile?關(guān)鍵字修飾這些變量:

          private volatile static int a, b, i, j = 0;

          這樣我們將無法再得到i和j均為0的情況了。

          happens-before原則

          由于有指令重排序的存在,這將導(dǎo)致我們在分析多線程程序的時候出現(xiàn)一些難以預(yù)料的問題,這些問題往往又很難被發(fā)現(xiàn)?;诖?,從JDK1.5開始,官方提出了happens-bofore原則,指的是如果一個線程對某個變量的操作在另一個線程對該變量的操作之前,則第一個線程的操作必須對第二個線程可見。happens-before共有六項規(guī)則:

          1.程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作;它表示在一個線程中的每個操作,對于其后續(xù)的操作都應(yīng)該是可見的2.監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖;它表示某個線程在解鎖之前的所有操作,都應(yīng)該對另一個加鎖的線程可見3.volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀;它表示某個線程在寫入一個volatile修飾的變量之前的所有操作都應(yīng)該對讀取這個volatile修飾的變量的線程可見4.傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C;它表示線程A對線程B可見,線程B對線程C可見,則線程A就對線程C可見5.start()規(guī)則:如果線程A執(zhí)行操作?ThreadB.start()?,那么線程A的?ThreadB.start()?操作happens-before于線程B的任意操作;它表示線程A在啟動線程B時,此時所有對共享變量的操作都對線程B可見,但線程B啟動之后,線程A再對共享變量進(jìn)行操作,線程B無法保證可見6.join()規(guī)則:如果線程A執(zhí)行操作?ThreadB.join()?并成功返回,那么線程B中的任意操作happens-before于線程A從?ThreadB.join()?操作成功返回;它表示線程B在調(diào)用?ThreadA.join()?并成功返回后,線程A所有對共享變量的操作都對線程B可見

          由此我們可知,對于剛才的案例,如果對四個變量添加了volatile關(guān)鍵字,則線程在對其進(jìn)行操作時,互相都是可見的,比如線程t1將a賦值為1的時候,線程t2讀取了變量a,那么線程t1在為變量a賦值及其之前的所有操作都將對線程t2可見,所以當(dāng)線程t1將變量a的值修改為1時,線程t2讀取到的變量i值一定為0,變量j值一定為1。

          基于happens-before原則,volatile重排序也受到了相應(yīng)的限制:

          ?當(dāng)對volatile變量進(jìn)行寫操作時,無論前一個操作是什么,都不能重排序?當(dāng)對volatile變量進(jìn)行讀操作時,無論后一個操作是什么,都不能重排序?當(dāng)先對volatile變量進(jìn)行寫操作,后進(jìn)行讀操作時,不能重排序

          volatile在單例模式中的應(yīng)用

          來溫習(xí)一下單例模式的書寫:

          publicclassSingletonDemo{privatestaticfinalSingletonDemosingletonDemo=newSingletonDemo();privateSingletonDemo(){}publicSingletonDemogetInstance(){returnsingletonDemo;}}

          這是餓漢式單例的一種寫法,還有懶漢式單例的實現(xiàn):

          publicclassSingletonDemo{privatestaticSingletonDemosingletonDemo;privateSingletonDemo(){}publicSingletonDemogetInstance(){if(singletonDemo==null){synchronized(SingletonDemo.class){if(singletonDemo==null){singletonDemo=newSingletonDemo();}}}returnsingletonDemo;}}

          這種方式通常被認(rèn)為是高效的、線程安全的,然而這種方式仍然面臨著一個問題,需要知道的是,對象的創(chuàng)建分為以下幾個步驟:


          1.JVM首先對new的內(nèi)容進(jìn)行解析,在常量池中查找一個類的符號引用2.若是沒有找到符號引用,則認(rèn)為該類是沒有被加載的,所以JVM會對其進(jìn)行類的加載、解析和初始化3.JVM為對象分配內(nèi)存4.將分配的內(nèi)存初始化為零值5.調(diào)用對象的方法

          這些步驟可以簡化為:分配內(nèi)存,初始化實例,返回引用?,F(xiàn)在假設(shè)線程A執(zhí)行到了?singletonDemo = new SingletonDemo()?,但由于創(chuàng)建對象的過程并不是一個原子性的操作,且編譯器可能會對創(chuàng)建對象的操作進(jìn)行重排序,所以當(dāng)JVM為對象分配了內(nèi)存之后,很有可能會將返回引用的操作提前,此時該引用還沒有進(jìn)行初始化等操作,接著線程B搶占到了執(zhí)行權(quán),其判斷singletonDemo不為空,就能夠直接獲取到singletonDemo的引用,但它僅僅是一個半成品,還沒有進(jìn)行接下來初始化的操作,此時線程B使用著這個半成品就會出現(xiàn)一些無法預(yù)料的問題。

          正確的辦法是使用volatile修飾單例變量:

          private static volatile SingletonDemo singletonDemo;

          這樣就能避免指令的重排序,使對象的創(chuàng)建步驟正常有序地進(jìn)行。

          volatile的應(yīng)用場景

          由于volatile的特性,使得它適用于一些純賦值的場景,對于一些非原子性的操作,比如:i++,volatile就不適合了,看一個例子:

          public class VolatileDemo {    public static void main(String[] args) throws InterruptedException {        MyThread myThread = new MyThread();        Thread t1 = new Thread(myThread);        Thread t2 = new Thread(myThread);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(myThread.flag);        System.out.println(myThread.atomicInteger);    }}
          class MyThread implements Runnable {
          public volatile boolean flag = false; public AtomicInteger atomicInteger = new AtomicInteger(0);
          @Override public void run() { for (int i = 0; i < 5000; i++) { change(); atomicInteger.incrementAndGet(); } }
          private void change() { flag = true; }}

          對于這個程序,輸出的flag結(jié)果永遠(yuǎn)只能是true,但是如果將?flag = true?修改為?flag = !flag?,這就不是一個原子性的操作了,此時程序就會出現(xiàn)true或false的兩種輸出結(jié)果。

          volatile還可用于變量的觸發(fā)器,前面我們了解過happens-before原則,當(dāng)某個volatile修飾的變量被賦值后,其它線程在獲取該變量時,該變量及其之前的操作對其它線程均是可見的,例如:

          public class VolatileDemo {
          private int a = 1; private int b = 2; private int c = 3; private volatile boolean flag = false;
          private void write() { a = 3; b = 4; c = 5; flag = true; }
          private void read() { while (flag) { System.out.println("a=" + a + ",b=" + b + ",c=" + c); } }
          public static void main(String[] args) throws InterruptedException { VolatileDemo volatileDemo = new VolatileDemo(); while (true) { new Thread(() -> { volatileDemo.write(); }).start(); new Thread(() -> { volatileDemo.read(); }).start(); } }}

          在read()方法中,當(dāng)flag值為true時會輸出變量a、b、c的值,而如果flag為true的話,那么為flag賦值之前的操作都將是可見的,所以變量a、b、c的值一定分別是3、4、5,這就相當(dāng)于一個觸發(fā)器,當(dāng)觸發(fā)器變量滿足條件時,刷新之前的變量得到最新值,觸發(fā)器典型的應(yīng)用場景就是Web容器的初始化。

          本文作者:汪偉俊?為Java技術(shù)迷專欄作者?投稿,未經(jīng)允許請勿轉(zhuǎn)載

          1、致歉!抖音Semi Design承認(rèn)參考阿里Ant Design

          2、對比7種分布式事務(wù)方案,還是偏愛阿里開源的Seata,真香!

          3、Redis存儲結(jié)構(gòu)體信息,選hash還是string?

          4、掃盲 docker 常用命令

          5、最全分布式Session解決方案

          6、21 款 yyds 的 IDEA插件

          7、真香!用 IDEA 神器看源碼,效率真高!

          點分享

          點收藏

          點點贊

          點在看

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  亚洲一二三四 | 亚洲第97页| 青娱乐成人视频 | 插吧,综合网 | 亚洲成人偷窥自拍 |