Android線程篇(九):關(guān)鍵字Volatile

九旬老太為何慘死街頭 數(shù)百頭母驢為何半夜慘叫 小賣部安全套為何屢遭黑手 女生宿舍內(nèi)褲為何頻頻失竊 連環(huán)強奸母豬案究竟是何人所為 老尼姑的門夜夜被敲究竟是人是鬼 數(shù)百頭母狗意外身亡背后又隱藏著什么 這一切的背后!!到底是人性的扭曲還是道德的淪喪?下面帶大家走進Java關(guān)鍵字之——volatile!

在多線程編程中,我們最常用的是synchronized,而對volatile的使用,卻對volatile的使用較少。這一方面是因為volatile的使用場景限制,另一方面是因為volatile使用需要更高的技術(shù)水平。

volatile關(guān)鍵字好多人都聽過,或許也使用過,從字面意思來看很好理解,但是要用好真的不是一件容易的事情。筆者在之前也看過好多大佬講過volatile,但是仍然沒有弄明白。自己在學(xué)習(xí)的過程中看了許多大神的博客,他們在講解Java內(nèi)存模型的時候總是把CPU的內(nèi)存架構(gòu)和Java內(nèi)存模型混為一談,事實上這倆個完全不是一個概念,CPU的高速緩存并不是主存的一部分,而是CPU本身自帶的,就像CPU里面的寄存器一樣。或許,他們都有自己的理解!在講解Volatile關(guān)鍵字之前,必須了解Java虛擬機的內(nèi)存模型和CUP的內(nèi)存架構(gòu),不了解的同學(xué)速度學(xué)習(xí)前幾篇。

Java內(nèi)存模型:
Android線程篇(五):Java內(nèi)存模型
CPU內(nèi)存架構(gòu):
Android線程篇(六):CPU內(nèi)存架構(gòu)
多線程下的緩存一致性問題:
Android線程篇(七):多線程下的緩存一致性問題
原子操作和指令重:
Android線程篇(八):原子操作和指令重排

volatile翻譯過來意思是:不穩(wěn)定的,易揮發(fā)的;

有道是:太極生兩儀,兩儀生四象,四象生八卦,天地造就萬物生靈,既然它存在,必然有它存在的意義,volatile到底有什么作用,意義何在?

部分摘自“海 子”博客,感謝作者:http://www.cnblogs.com/dolphin0520/p/3920373.html

1.可見性

Java提供了volatile關(guān)鍵字來保證可見性。

當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。

而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性。

另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

2.有序性

在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。

在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”(具體原理在下一節(jié)講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。

另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。

下面就來具體介紹下happens-before原則(先行發(fā)生原則):

  • 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
  • 鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖unLock操作
  • volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
  • 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
  • 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作
  • 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
  • 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行
  • 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
    這8條原則摘自《深入理解Java虛擬機》。

這8條規(guī)則中,前4條規(guī)則是比較重要的,后4條規(guī)則都是顯而易見的。

下面我們來解釋一下前4條規(guī)則:

對于程序次序規(guī)則來說,我的理解就是一段程序代碼的執(zhí)行在單個線程中看起來是有序的。注意,雖然這條規(guī)則中提到“書寫在前面的操作先行發(fā)生于書寫在后面的操作”,這個應(yīng)該是程序看起來執(zhí)行的順序是按照代碼順序執(zhí)行的,因為虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致的,它只會對不存在數(shù)據(jù)依賴性的指令進行重排序。因此,在單個線程中,程序執(zhí)行看起來是有序執(zhí)行的,這一點要注意理解。事實上,這個規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。

第二條規(guī)則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出于被鎖定的狀態(tài),那么必須先對鎖進行了釋放操作,后面才能繼續(xù)進行l(wèi)ock操作。

第三條規(guī)則是一條比較重要的規(guī)則,也是后文將要重點講述的內(nèi)容。直觀地解釋就是,如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發(fā)生于讀操作。

第四條規(guī)則實際上就是體現(xiàn)happens-before原則具備傳遞性。

學(xué)習(xí)了volatile的作用之后,我們繼續(xù)上上篇文章的例子來看:

    public int count = 0;
    public int TestVolatile(){
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                    }

                    count++;
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("<<<<<"+count);
        return count;
    }

volatile具有可見性,給count加上volatile就線程安全了,輸出的結(jié)果就是我們所期望的結(jié)果,事實真的如此嗎?

public volatile int count = 0;
    public  int TestVolatile() {
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                    }

                    increase();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("<<<<<" + count);
        return count;
    }
    public void increase() {
        count++;

    }

變量count被使用了volatile修飾,那么在thread1中,當count變?yōu)?的時候,就會強制刷新到主存。如果這個時候,thread2已經(jīng)將count =2從從主存映射到緩存上,那么在對count進行自增操作以前,會重新到主存中讀取count =3,然后自增到count =4,然后寫回到主存。上面的過程很完美,但這樣是否保證了count最終的結(jié)果一定是4呢?

當然,結(jié)果要讓大家失望了。

我們來分析下為什么?
來看看count++操作的時候內(nèi)存都做了什么操作:
1.從主內(nèi)存里面(棧)讀取到count的值,到CPU的高速緩存當中
2.寄存器對count的值進行加一操作
3.將CPU的高速緩存當中的count值刷新到主內(nèi)存(棧)當中

我們可以看到++這個操作非原子,先讀count,然后+1, 最后再寫 count

如果變量count被使用了volatile修飾,那么在thread1中,當count變?yōu)?的時候,就會強制刷新到主存。如果這個時候,thread2已經(jīng)將count =2從從主存映射到緩存上并且已經(jīng)做完了自增操作,此時count =3,那么最終主存中count值為3。

所以,如果我們想讓count的最終值是4,僅僅保證可見性是不夠的,還得保證原子性。也就是對于變量count的自增操作加鎖,保證任意一個時刻只有一個線程對count進行自增操作。可以說volatile是一種“輕量級的鎖”,它能保證鎖的可見性,但不能保證鎖的原子性。
具體解決辦法請移步:
多線程下的緩存一致性問題:
Android線程篇(七):多線程下的緩存一致性問題

繼續(xù)來一個例子:

//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
//線程2
stop = true;

這段代碼很典型,很多人都會采用這種標記辦法來處理是否進入循環(huán)。但是事實上,這段代碼會完全運行正確么?不一定,也許在大多數(shù)時候是正確的,但是也有可能是錯誤的(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。

下面解釋一下這段代碼為何會有問題。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當中。

那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。

但是用volatile修飾stop之后就變得不一樣了:

//線程1
volatile boolean stop = false;
while(!stop){
    doSomething();
}
//線程2
stop = true;

第一:使用volatile關(guān)鍵字會強制將修改的值立即寫入主存;

第二:使用volatile關(guān)鍵字的話,當線程2進行修改時,會導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效,也就是執(zhí)行線程1的CPU緩存中的stop無效。

第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。

那么在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應(yīng)的主存地址被更新之后,然后去對應(yīng)的主存讀取最新的值。

那么線程1讀取到的就是最新的正確的值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容