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

          面試必問的 volatile 關(guān)鍵字,一篇給你安排了?。。?/h1>

          共 12330字,需瀏覽 25分鐘

           ·

          2021-06-01 10:31


          volatile 這個(gè)關(guān)鍵字大家都不陌生,這個(gè)關(guān)鍵字一般通常用于并發(fā)編程中,是 Java 虛擬機(jī)提供的輕量化同步機(jī)制,你可能知道 volatile 是干啥的,但是你未必能夠清晰明了的知道 volatile 的實(shí)現(xiàn)機(jī)制,以及 volatile 解決了什么問題,這篇文章我就來帶大家解析一波。

          volatile 能夠保證共享變量之間的 可見性,共享變量是存在堆區(qū)的,而堆區(qū)又與內(nèi)存模型有關(guān),所以我們要聊 volatile ,就需要首先了解一下  內(nèi)存模型。Java 中的內(nèi)存模型是 JVM 提供的,而 JVM 又是和內(nèi)存進(jìn)行交互的,所以在聊  內(nèi)存模型前,我們還需要了解一下操作系統(tǒng)層面中內(nèi)存模型的相關(guān)概念。

          先從內(nèi)存模型談起

          計(jì)算機(jī)在執(zhí)行程序時(shí),會(huì)從內(nèi)存中讀取數(shù)據(jù),然后加載到 CPU 中運(yùn)行。由于 CPU 執(zhí)行指令的速度要比從內(nèi)存中讀取和寫入的速度快的多,所以如果每條指令都要和內(nèi)存交互的話,會(huì)大大降低 CPU 的運(yùn)行速度,造成昂貴的 CPU 性能損耗,為了解決這種問題,設(shè)計(jì)了 CPU 高速緩存。有了 CPU 高速緩存后,CPU 就不再需要頻繁的和內(nèi)存交互了,有高速緩存就行了,而 CPU 高速緩存,就是我們經(jīng)常說的 L1 、L2、L3 cache。

          當(dāng)程序在運(yùn)行過程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到 CPU 的高速緩存中,在 CPU 進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀寫數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存中。

          就拿我們常說的

          i = i + 1 

          來舉例子

          當(dāng) CPU 執(zhí)行這條語句時(shí),會(huì)先從內(nèi)存中讀取 i 的值,復(fù)制一份到高速緩存當(dāng)中,然后 CPU 執(zhí)行指令對(duì) i 進(jìn)行加 1 操作,再將數(shù)據(jù)寫入高速緩存,最后將高速緩存中 i 最新的值刷新到主存當(dāng)中。

          這個(gè)代碼在單線程中運(yùn)行是沒有任何問題的,但是在多線程中運(yùn)行就會(huì)有問題了,因?yàn)槊總€(gè) CPU 都可以運(yùn)行一條線程,線程就是程序的順序執(zhí)行流,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核 CPU 來說,其實(shí)也會(huì)出現(xiàn)這種問題,只不過是以線程調(diào)度的形式來分別執(zhí)行的)。本文我們以多核 CPU 為例來講解說明。

          比如同時(shí)有 2 個(gè)線程執(zhí)行這段代碼,假如初始時(shí) i 的值為 0,那么我們希望兩個(gè)線程執(zhí)行完之后 i 的值變?yōu)?2,但是事實(shí)會(huì)是這樣嗎?

          可能存在下面一種情況:初始時(shí),兩個(gè)線程分別讀取 i 的值存入各自所在的 CPU 高速緩存中,然后線程 1 執(zhí)行加 1 操作,把 i 的最新值 1 寫入到內(nèi)存。此時(shí)線程 2 的高速緩存當(dāng)中 i 的值還是 0,進(jìn)行加 1 操作之后,i 的值為 1,然后線程 2 把 i 的值寫入內(nèi)存。

          最終結(jié)果 i 的值是 1,而不是 2。這就是著名的緩存一致性問題。通常稱這種被多個(gè)線程訪問的變量為共享變量。

          也就是說,如果一個(gè)變量在多個(gè) CPU 中都存在緩存(一般在多線程編程時(shí)才會(huì)出現(xiàn)),就很可能存在緩存不一致的問題。

          JVM 內(nèi)存模型

          我們上面說到,共享變量會(huì)存在緩存不一致的問題,緩存不一致問題換種說法就是線程安全問題,那么共享變量在 Java 中是如何存在的呢?JVM 有沒有提供線程安全的變量或者數(shù)據(jù)呢?

          這就要聊聊 JVM 內(nèi)存模型的問題了,圖示如下

          • 虛擬機(jī)棧 : Java 虛擬機(jī)棧是線程私有的數(shù)據(jù)區(qū),Java 虛擬機(jī)棧的生命周期與線程相同,虛擬機(jī)棧也是局部變量的存儲(chǔ)位置。方法在執(zhí)行過程中,會(huì)在虛擬機(jī)棧創(chuàng)建一個(gè) 棧幀(stack frame)。

          • 本地方法棧: 本地方法棧也是線程私有的數(shù)據(jù)區(qū),本地方法棧存儲(chǔ)的區(qū)域主要是 Java 中使用 native 關(guān)鍵字修飾的方法所存儲(chǔ)的區(qū)域。

          • 程序計(jì)數(shù)器:程序計(jì)數(shù)器也是線程私有的數(shù)據(jù)區(qū),這部分區(qū)域用于存儲(chǔ)線程的指令地址,用于判斷線程的分支、循環(huán)、跳轉(zhuǎn)、異常、線程切換和恢復(fù)等功能,這些都通過程序計(jì)數(shù)器來完成。

          • 方法區(qū):方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)虛擬機(jī)加載的 類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

          • :堆是線程共享的數(shù)據(jù)區(qū),堆是 JVM 中最大的一塊存儲(chǔ)區(qū)域,所有的對(duì)象實(shí)例都會(huì)分配在堆上

          • 運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池又被稱為 Runtime Constant Pool,這塊區(qū)域是方法區(qū)的一部分,它的名字非常有意思,它并不要求常量一定只有在編譯期才能產(chǎn)生,也就是并非編譯期間將常量放在常量池中,運(yùn)行期間也可以將新的常量放入常量池中,String 的 intern 方法就是一個(gè)典型的例子。

          根據(jù)上面的描述可以看到,會(huì)產(chǎn)生緩存不一致問題(線程安全問題)的有堆區(qū)和方法區(qū)。而虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器是線程私有,由線程封閉的原因,它們不存在線程安全問題。

          針對(duì)線程安全問題,有沒有解決辦法呢?

          一般情況下,Java 中解決緩存不一致的方法有兩種,第一種就是 synchronized 使用的總線鎖方式,也就是在總線上聲言 LOCK# 信號(hào);第二種就是著名的 MESI 協(xié)議。這兩種都是硬件層面提供的解決方式。

          我們先來說一下第一種總線鎖的方式。通過在總線上聲言 LOCK# 信號(hào),能夠有效的阻塞其他 CPU 對(duì)于總線的訪問,從而使得只能有一個(gè) CPU 訪問變量所在的內(nèi)存。在上面的 i = i + 1 代碼示例中,在代碼執(zhí)行的過程中,聲言了 LOCK# 信號(hào)后,那么只有等待 i = i + 1 的結(jié)果執(zhí)行完畢并應(yīng)用到內(nèi)存后,總線鎖才會(huì)解開,其他 CPU 才能夠繼續(xù)訪問內(nèi)存中的變量,再繼續(xù)執(zhí)行后面的代碼,這樣就解決了緩存不一致問題。

          但是上面的方式會(huì)有一個(gè)問題,由于在鎖住總線期間,其他 CPU 無法訪問內(nèi)存,導(dǎo)致效率低下。

          在 JDK 1.6 之后,優(yōu)化了 synchronized 聲言 LOCK# 的方式,不再對(duì)總線進(jìn)行鎖定,轉(zhuǎn)而采取了對(duì) CPU 緩存行進(jìn)行鎖定,因?yàn)楸酒恼虏皇墙榻B synchronized 實(shí)現(xiàn)細(xì)節(jié)的文章,所以不再對(duì)這種方式進(jìn)行詳細(xì)介紹,讀者只需要知道在優(yōu)化之后,synchronized 的性能不再成為并發(fā)問題的瓶頸了。

          MESI 協(xié)議就是緩存一致性協(xié)議,即 Modified(被修改)Exclusive(獨(dú)占的) Shared(共享的) Or Invalid(無效的)。MESI 的基本思想就是如果發(fā)現(xiàn) CPU 操作的是共享變量,其他 CPU 中也會(huì)出現(xiàn)這個(gè)共享變量的副本,在 CPU 執(zhí)行代碼期間,會(huì)發(fā)出信號(hào)通知其他 CPU 自己正在修改共享變量,其他 CPU 收到通知后就會(huì)把自己的共享變量置為無效狀態(tài)。

          并發(fā)編程中的三個(gè)主要問題

          可見性問題

          在單核 CPU 時(shí)代,所有的線程共用一個(gè) CPU,CPU 緩存和內(nèi)存的一致性問題容易解決,我們還拿上面的 i = 1 + 1 來舉例,CPU 和 內(nèi)存之間如果用圖來表示的話我想會(huì)是下面這樣。

          在多核時(shí)代,每個(gè)核都能夠獨(dú)立的運(yùn)行一個(gè)線程,每個(gè) CPU 都有自己的緩存,這時(shí) CPU 緩存與內(nèi)存的數(shù)據(jù)一致性就沒那么容易解決了,當(dāng)多個(gè)線程在不同的 CPU 上執(zhí)行時(shí),這些線程使用的是不同的 CPU 緩存。

          因?yàn)?i 沒有經(jīng)過任何線程安全措施的保護(hù),多個(gè)線程會(huì)并發(fā)修改 i 的值,所以我們認(rèn)為 i 不是線程安全的,導(dǎo)致這種結(jié)果的出現(xiàn)是由于 aThread 和 bThread 中讀取的 i 值彼此不可見,所以這是由于 可見性 導(dǎo)致的線程安全問題。

          原子性問題

          當(dāng)兩個(gè)線程開始運(yùn)行后,每個(gè)線程都會(huì)把 i 的值讀入到 CPU 緩存中,再執(zhí)行 + 1 操作,然后把 + 1 之后的值寫入內(nèi)存。因?yàn)榫€程間都有各自的虛擬機(jī)棧和程序計(jì)數(shù)器,他們彼此之間沒有數(shù)據(jù)交換,所以當(dāng) aThread 執(zhí)行 + 1 操作后,會(huì)把數(shù)據(jù)寫入到內(nèi)存,同時(shí) bThread 執(zhí)行 + 1 操作后,也會(huì)把數(shù)據(jù)寫入到內(nèi)存,因?yàn)?CPU 時(shí)間片的執(zhí)行周期是不確定的,所以會(huì)出現(xiàn)當(dāng) aThread 還沒有把數(shù)據(jù)寫入內(nèi)存時(shí),bThread 就會(huì)讀取內(nèi)存中的數(shù)據(jù),然后執(zhí)行 + 1操作,再寫回內(nèi)存,從而覆蓋 i 的值。

          有序性問題

          在并發(fā)編程中還有帶來讓人非常頭疼的 有序性 問題,有序性顧名思義就是順序性,在計(jì)算機(jī)中指的就是指令的先后執(zhí)行順序。一個(gè)非常顯而易見的例子就是 JVM 中的類加載。

          這是一個(gè) JVM 加載類的過程圖,也稱為類的生命周期,類從加載到 JVM 到卸載一共會(huì)經(jīng)歷五個(gè)階段 加載、連接、初始化、使用、卸載。這五個(gè)過程的執(zhí)行順序是一定的,但是在連接階段,也會(huì)分為三個(gè)過程,即 驗(yàn)證、準(zhǔn)備、解析 階段,這三個(gè)階段的執(zhí)行順序不是確定的,通常交叉進(jìn)行,在一個(gè)階段的執(zhí)行過程中會(huì)激活另一個(gè)階段。

          在執(zhí)行程序的過程中,為了提高性能,編譯器和處理器通常會(huì)對(duì)指令進(jìn)行重排序。重排序主要分為三類

          • 編譯器優(yōu)化的重排序:編譯器在不改變單線程語義的情況下,會(huì)對(duì)執(zhí)行語句進(jìn)行重新排序。

          • 指令集重排序:現(xiàn)代操作系統(tǒng)中的處理器都是并行的,如果執(zhí)行語句之間不存在數(shù)據(jù)依賴性,處理器可以改變語句的執(zhí)行順序

          • 內(nèi)存重排序:由于處理器會(huì)使用讀/寫緩沖區(qū),出于性能的原因,內(nèi)存會(huì)對(duì)讀/寫進(jìn)行重排序

          也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。

          volatile 的實(shí)現(xiàn)原理

          上面聊了這么多,你可能都要忘了這篇文章的故事主角了吧?主角永遠(yuǎn)存在于我們心中 ……

          其實(shí)上面聊的這些,都是在為 volatile 做鋪墊。

          在并發(fā)編程中,最需要處理的就是線程之間的通信和線程間的同步問題,上面的可見性、原子性、有序性也是這兩個(gè)問題帶來的。

          可見性

          而 volatile 就是為了解決這些問題而存在的。Java 語言規(guī)范對(duì) volatile 下了定義:Java 語言為了確保能夠安全的訪問共享變量,提供了 volatile 這個(gè)關(guān)鍵字,volatile 是一種輕量級(jí)同步機(jī)制,它并不會(huì)對(duì)共享變量進(jìn)行加鎖,但在某些情況下要比加鎖更加方便,如果一個(gè)字段被聲明為 volatile,Java 線程內(nèi)存模型能夠確保所有線程訪問這個(gè)變量的值都是一致的。

          一旦共享變量被 volatile 修飾后,就具有了下面兩種含義

          1. 保證了這個(gè)字段的可見性,也就是說所有線程都能夠"看到"這個(gè)變量的值,如果某個(gè) CPU 修改了這個(gè)變量的值之后,其他 CPU 也能夠獲得通知。

          2. 能夠禁止指令的重排序

          下面我們來看一段代碼,這也是我們編寫并發(fā)代碼中經(jīng)常會(huì)使用到的

          boolean isStop = false;
          while(!isStop){
              ...
          }

          isStop = true;

          在這段代碼中,如果線程一正在執(zhí)行 while 循環(huán),而線程二把 isStop 改為 true 之后,轉(zhuǎn)而去做其他事情,因?yàn)榫€程一并不知道線程二把 isStop 改為 true ,所以線程一就會(huì)一直運(yùn)行下去。

          如果 isStop 用 volatile 修飾之后,那么事情就會(huì)變的不一樣了。

          使用 volatile 修飾了 isStop 之后,在線程二把 isStop 改為 true 之后,會(huì)強(qiáng)制將其寫入內(nèi)存,并且會(huì)把線程一中 isStop 的值置為無效(這個(gè)值實(shí)際上是在緩存在 CPU 中的緩存行里),當(dāng)線程一繼續(xù)執(zhí)行代碼的時(shí)候,會(huì)從內(nèi)存中重新讀取 isStop 的值,此時(shí) isStop 的值就是正確的內(nèi)存地址的值。

          volatile 有下面兩條實(shí)現(xiàn)原則,其實(shí)這兩條原則我們在上面介紹的時(shí)候已經(jīng)提過了,一種是總線鎖的方式,我們后面說總線鎖的方式開銷比較大,所以后面設(shè)計(jì)人員做了優(yōu)化,采用了鎖緩存的方式。另外一種是 MESI 協(xié)議的方式。

          • 在 IA-32 架構(gòu)軟件開發(fā)者的手冊中,有一種 Lock 前綴指令,這種指令能夠聲言 LOCK# 信號(hào),在最近的處理器中,LOCK# 信號(hào)用于鎖緩存,等到指令執(zhí)行完畢后,會(huì)把緩存的內(nèi)容寫回內(nèi)存,這種操作一般又被稱為緩存鎖定

          • 當(dāng)緩存寫回內(nèi)存后,IA-32 和 IA-64 處理器會(huì)使用 MESI 協(xié)議控制內(nèi)部緩存和其他處理器一致。IA-32 和 IA-64 處理器能夠嗅探其他處理器訪問系統(tǒng)內(nèi)部緩存,當(dāng)內(nèi)存值修改后,處理器會(huì)從內(nèi)存中重新讀取內(nèi)存值進(jìn)行新的緩存行填充。

          由此可見,volatile 能夠保證線程的可見性。

          那么 volatile 能夠保證原子性嗎?

          原子性

          我們還是以 i = i + 1 這個(gè)例子來說明一下,i = i + 1 分為三個(gè)操作

          • 讀取 i 的值

          • 自增 i 的值

          • 把 i 的值寫內(nèi)存

          我們知道,volatile 能夠保證修改 i 的值對(duì)其他線程可見,所以我們此時(shí)假設(shè)線程一執(zhí)行 i 的讀取操作,此時(shí)發(fā)生了線程切換,線程二讀取到最新 i 的值是 0 ,然后線程再次發(fā)生切換,線程一把 i 的值改為 1,線程再次切換,因?yàn)榇藭r(shí) i 的值還沒有應(yīng)用到內(nèi)存,所以線程 i 同樣把 i 的值改為 1 后,線程再次發(fā)生切換,線程一把 i 的值寫入內(nèi)存后,再次發(fā)生切換,線程二再次把 i 的值寫會(huì)內(nèi)存,所以此時(shí),雖然內(nèi)存值改了兩次,但是最后的結(jié)果卻不是 2。

          那么 volatile 不能保證原子性,那么該如何保證原子性呢?

          在 JDK 5 的 java.util.concurrent.atomic 包下提供了一些原子操作類,例如 AtomicInteger、AtomicLong、AtomicBoolean,這些操作是原子性操作。它們是利用 CAS 來實(shí)現(xiàn)原子性操作的(Compare And Swap),CAS實(shí)際上是利用處理器提供的 CMPXCHG 指令實(shí)現(xiàn)的,而處理器執(zhí)行 CMPXCHG 指令是一個(gè)原子性操作。

          詳情可以參考筆者的這篇文章 一場 Atomic XXX 的魔幻之旅。

          那么 volatile 能不能保證有序性呢?

          這里就需要和你聊一聊 volatile 對(duì)有序性的影響了

          有序性

          上面提到過,重排序分為編譯器重排序、處理器重排序和內(nèi)存重排序。我們說的 volatile 會(huì)禁用指令重排序,實(shí)際上 volatile 禁用的是編譯器重排序和處理器重排序。

          下面是 volatile 禁用重排序的規(guī)則

          從這個(gè)表中可以看出來,讀寫操作有四種,即不加任何修飾的普通讀寫和使用 volatile 修飾的讀寫。

          從這個(gè)表中,我們可以得出下面這些結(jié)論

          • 只要第二個(gè)操作(這個(gè)操作就指的是代碼執(zhí)行指令)是 volatile 修飾的寫操作,那么無論第一個(gè)操作是什么,都不能被重排序。

          • 當(dāng)?shù)谝粋€(gè)操作是 volatile 讀時(shí),不管第二個(gè)操作是什么,都不能進(jìn)行重排序。

          • 當(dāng)?shù)谝粋€(gè)操作是 volatile 寫之后,第二個(gè)操作是 volatile 讀/寫都不能重排序。

          為了實(shí)現(xiàn)這種有序性,編譯器會(huì)在生成字節(jié)碼中,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。

          這里我們先來了解一下內(nèi)存屏障的概念。

          內(nèi)存屏障也叫做柵欄,它是一種底層原語。它使得 CPU 或編譯器在對(duì)內(nèi)存進(jìn)行操作的時(shí)候, 要嚴(yán)格按照一定的順序來執(zhí)行, 也就是說在 memory barrier 之前的指令和 memory barrier 之后的指令不會(huì)由于系統(tǒng)優(yōu)化等原因而導(dǎo)致亂序。

          內(nèi)存屏障提供了兩個(gè)功能。首先,它們通過確保從另一個(gè) CPU 來看屏障的兩邊的所有指令都是正確的程序順序;其次它們可以實(shí)現(xiàn)內(nèi)存數(shù)據(jù)可見性,確保內(nèi)存數(shù)據(jù)會(huì)同步到 CPU 緩存子系統(tǒng)。

          不同計(jì)算機(jī)體系結(jié)構(gòu)下面的內(nèi)存屏障也不一樣,通常需要認(rèn)真研讀硬件手冊來確定,所以我們的主要研究對(duì)象是基于 x86 的內(nèi)存屏障,通常情況下,硬件為我們提供了四種類型的內(nèi)存屏障。

          • LoadLoad 屏障

          它的執(zhí)行順序是 Load1 ;LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加載指令。LoadLoad 指令能夠確保執(zhí)行順序是在 Load1 之后,Load2 之前,LoadLoad 指令是一個(gè)比較有效的防止看到舊數(shù)據(jù)的指令。

          • StoreStore 屏障

          它的執(zhí)行順序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的執(zhí)行順序相似,它也能夠確保執(zhí)行順序是在 Store1 之后,Store2 之前。

          • LoadStore 屏障

          它的執(zhí)行順序是 Load1 ;StoreLoad ;Store2 ,保證 Load1 的數(shù)據(jù)被加載在與這數(shù)據(jù)相關(guān)的 Store2 和之后的 store 指令之前。

          • StoreLoad 屏障

          它的執(zhí)行順序是 Store1 ;StoreLoad ;Load2 ,保證 Store1 的數(shù)據(jù)被其他 CPU 看到,在數(shù)據(jù)被 Load2 和之后的 load 指令加載之前。也就是說,它有效的防止所有 barrier 之前的 stores 與所有 barrier 之后的 load 亂序。

          JMM 采取了保守策略來實(shí)現(xiàn)內(nèi)存屏障,JMM 使用的內(nèi)存屏障如下

          下面是一個(gè)使用內(nèi)存屏障的示例

          class MemoryBarrierTest {
            int a, b;
            volatile int v, u;
            void f() {
              int i, j;

              i = a;
              j = b;
              i = v;

              j = u;

              a = i;
              b = j;

              v = i;

              u = j;

              i = u;

              j = b;
              a = i;
            }
          }

          這段代碼雖然比較簡單,但是使用了不少變量,看起來有些亂,我們反編譯一下來分析一下內(nèi)存屏障對(duì)這段代碼的影響。

          從反編譯的代碼我們是看不到內(nèi)存屏障的,因?yàn)閮?nèi)存屏障是一種硬件層面的指令,單憑字節(jié)碼是肯定無法看到的。雖然無法看到內(nèi)存屏障的硬件指令,但是 JSR-133 為我們說明了哪些字節(jié)碼會(huì)出現(xiàn)內(nèi)存屏障。

          • 普通的讀類似 getfield 、getstatic 、 不加 volatile 修飾的數(shù)組 load 。

          • 普通的寫類似 putfield 、 putstatic 、 不加 volatile 修飾的數(shù)組 store 。

          • volatile 讀是可以被多個(gè)線程訪問修飾的 getfield、 getstatic 字段。

          • volatile 寫是可以被當(dāng)個(gè)線程訪問修飾的 putfield、 putstatic 字段。

          這也就是說,只要是普通的讀寫加上了 volatile 關(guān)鍵字之后,就是 volatile 讀寫(呃呃呃,我好像說了一句廢話),并沒有其他特殊的 volatile 獨(dú)有的指令。

          根據(jù)這段描述,我們來繼續(xù)分析一下上面的字節(jié)碼。

          a、b 是全局變量,也就是實(shí)例變量,不加 volatile 修飾,u、v 是 volatile 修飾的全局變量;i、j 是局部變量。

          首先 i = a、j = b 只是把全局變量的值賦給了局部變量,由于是獲取對(duì)象引用的操作,所以是字節(jié)碼指令是 getfield 。

          從官方手冊就可以知曉原因了。

          地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

          由內(nèi)存屏障的表格可知,第一個(gè)操作是普通讀寫的情況下,只有第二個(gè)操作是 volatile 寫才會(huì)設(shè)置內(nèi)存屏障。

          繼續(xù)向下分析,遇到了 i = v,這個(gè)是把 volatile 變量賦值給局部變量,是一種 volatile 讀,同樣的 j = u 也是一種 volatile 讀,所以這兩個(gè)操作之間會(huì)設(shè)置 LoadLoad 屏障。

          下面遇到了 a = i ,這是為全局變量賦值操作,所以其對(duì)應(yīng)的字節(jié)碼是 putfield

          地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

          所以在 j = u 和 a = i 之間會(huì)增加 LoadStore 屏障。然后 a = i 和 b = j 是兩個(gè)普通寫,所以這兩個(gè)操作之間不需要有內(nèi)存屏障。

          繼續(xù)往下面分析,第一個(gè)操作是 b = j ,第二個(gè)操作是 v = i 也就是 volatile 寫,所以需要有 StoreStore 屏障;同樣的,v = i 和 u = j 之間也需要有 StoreStore 屏障。

          第一個(gè)操作是 u = j 和 第二個(gè)操作 i = u volatile 讀之間需要 StoreLoad 屏障。

          最后一點(diǎn)需要注意下,因?yàn)樽詈髢蓚€(gè)操作是普通讀和普通寫,所以最后需要插入兩個(gè)內(nèi)存屏障,防止 volatile 讀和普通讀/寫重排序。

          《Java 并發(fā)編程藝術(shù)》里面也提到了這個(gè)關(guān)鍵點(diǎn)。

          從上面的分析可知,volatile 實(shí)現(xiàn)有序性是通過內(nèi)存屏障來實(shí)現(xiàn)的。

          關(guān)鍵概念

          在 volatile 實(shí)現(xiàn)可見性和有序性的過程中,有一些關(guān)鍵概念,cxuan 這里重新給讀者朋友們嘮叨下。

          • 緩沖行:英文概念是 cache line,它是緩存中可以分配的最小存儲(chǔ)單位。因?yàn)閿?shù)據(jù)在內(nèi)存中不是以獨(dú)立的項(xiàng)進(jìn)行存儲(chǔ)的,而是以臨近 64 字節(jié)的方式進(jìn)行存儲(chǔ)。

          • 緩存行填充:cache line fill,當(dāng) CPU 把內(nèi)存的數(shù)據(jù)載入緩存時(shí),會(huì)把臨近的共 64 字節(jié)的數(shù)據(jù)一同放入同一個(gè) Cache line,因?yàn)榫植啃栽恚号R近的數(shù)據(jù)在將來被訪問的可能性大。

          • 緩存命中:cache hit,當(dāng) CPU 從內(nèi)存地址中提取數(shù)據(jù)進(jìn)行緩存行填充時(shí),發(fā)現(xiàn)提取的位置仍然是上次訪問的位置,此時(shí) CPU 會(huì)選擇從緩存中讀取操作數(shù),而不是從內(nèi)存中取。

          • 寫命中:write hit ,當(dāng)處理器打算將操作數(shù)寫回到內(nèi)存時(shí),首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器會(huì)將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這種方式被稱為寫命中。

          • 內(nèi)存屏障:memory barriers,是一組硬件指令,是 volatile 實(shí)現(xiàn)有序性的基礎(chǔ)。

          • 原子操作:atomic operations,是一組不可中斷的一個(gè)或者一組操作。

          如何正確的使用 volatile 變量

          上面我們聊了這么多 volatile 的原理,下面我們就來談一談 volatile 的使用問題。

          volatile 通常用來和 synchronized 鎖進(jìn)行比較,雖然它和鎖都具有可見性,但是 volatile 不具有原子性,它不是真正意義上具有線程安全性的一種工具。

          從程序代碼簡易性和可伸縮性角度來看,你可能更傾向于使用 volatile 而不是鎖,因?yàn)?volatile 寫起來更方便,并且 volatile 不會(huì)像鎖那樣造成線程阻塞,而且如果程序中的讀操作的使用遠(yuǎn)遠(yuǎn)大于寫操作的話,volatile 相對(duì)于鎖還更加具有性能優(yōu)勢。

          很多并發(fā)專家都推薦遠(yuǎn)離 volatile 變量,因?yàn)樗鼈兿鄬?duì)于鎖更加容易出錯(cuò),但是如果你謹(jǐn)慎的遵從一些模式,就能夠安全的使用 volatile 變量,這里有一個(gè) volatile 使用原則

          只有在狀態(tài)真正獨(dú)立于程序內(nèi)其他內(nèi)容時(shí)才能使用 volatile。

          下面我們通過幾段代碼來感受一下這條規(guī)則的力量。

          狀態(tài)標(biāo)志

          一種最簡單使用 volatile 的方式就是將 volatile 作為狀態(tài)標(biāo)志來使用。

          volatile boolean shutdownRequested;

          public void shutdown() { shutdownRequested = true; }

          public void doWork() 
              while (!shutdownRequested) { 
                  // do stuff
              }
          }

          為了能夠正確的調(diào)用 shutdown() 方法,你需要確保 shutdownRequested 的可見性。這種狀態(tài)標(biāo)志的一種特性就是通常只有一種狀態(tài)轉(zhuǎn)換:shutdownRequested 的標(biāo)志從 false 轉(zhuǎn)為 true,然后程序停止。這種模式可以相互來回轉(zhuǎn)換。

          雙重檢查鎖

          使用 volatile 和 synchronized 可以滿足雙重檢查鎖的單例模式。

          class Singleton{

              private volatile static Singleton instance = null;
              private Singleton() {}

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

          這里說下為什么要用兩次檢查,假如有兩個(gè)線程,線程一在進(jìn)入到 synchronized 同步代碼塊之后,在還沒有生成 Singleton 對(duì)象前發(fā)生線程切換,此時(shí)線程二判斷 instance == null 為 true,會(huì)發(fā)生線程切換,切換到線程一,然后退出同步代碼塊,線程切換,線程二進(jìn)入同步代碼塊后,會(huì)再判斷一下 instance 的值,這就是雙重檢查鎖的必要所在。

          讀-寫鎖

          這也是 volatile 和 synchronized 一起使用的示例,用于實(shí)現(xiàn)開銷比較低的讀-寫鎖。

          public class ReadWriteLockTest {
                  private volatile int value;

              public int getValue() return value; }

              public synchronized int increment() {
                  return value++;
              }
          }

          如果只使用 volatile 是不能安全實(shí)現(xiàn)計(jì)數(shù)器的,但是你能夠在讀操作中使用 volatile 保證可見性。如果你想要實(shí)現(xiàn)一種讀寫鎖的話,必須進(jìn)行外部加鎖。

          另外,cxuan 肝了六本 PDF,公號(hào)回復(fù) cxuan ,領(lǐng)取作者全部 PDF 。



          瀏覽 44
          點(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>
                  无码中文字幕视频在线观看 | 欧美在线三级 | 超碰爱爱 | 偷拍福利视频网站 | 中国性老太HD大全120 |