免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
你真的了解volatile嗎?

 無論是在面試時(shí),還是在實(shí)際開發(fā)中,高并發(fā)問題已經(jīng)成為了現(xiàn)在的主旋律。

  并發(fā)問題的定位和重現(xiàn)是一件很棘手且難以解決的事情,為了盡可能的減少并發(fā)問題的產(chǎn)生,正確的編寫并發(fā)程序顯得尤其重要。

  解決并發(fā)問題,我們一般需要從原子性、可見性和有序性三方面入手,借助Java關(guān)鍵字及各種同步工具類來實(shí)現(xiàn)。

  原子性、可見性、有序性三特性:

  原子性:原子性就是說一個(gè)操作不能被打斷,要么執(zhí)行完要么不執(zhí)行。

  可見性:可見性是指一個(gè)變量的修改對(duì)所有線程可見。即當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來說是可以立即得知的。

  有序性:為了提高程序的執(zhí)行性能,編輯器和處理器都有可能會(huì)對(duì)程序中的指令進(jìn)行重排序。

  其中,volatile作為Java中最輕量級(jí)的同步機(jī)制,可以被用來解決實(shí)例屬性的可見性問題。

  volatile的兩種特性,決定了它的作用

  volatile關(guān)鍵字是Java提供的最輕量級(jí)的同步機(jī)制,為字段的訪問提供了一種免鎖機(jī)制,使用它不會(huì)引起線程的切換及調(diào)度。

  一個(gè)變量被定義為volatile之后就具備了兩種特性:

  可見性:簡單地說就是volatile變量修改后,所有線程都能立即實(shí)時(shí)地看到它的最新值。

  有序性:指系統(tǒng)在進(jìn)行代碼優(yōu)化時(shí),不能把在volatile變量操作后面的語句放到其前面執(zhí)行,也不能將volatile變量操作前面的語句放在其后執(zhí)行。

  Java中的volatile關(guān)鍵字可以解決多線程可見性問題。那它是何時(shí)以及如何使用呢?

  下面我們一起來揭秘。

  初識(shí)Volatile:保證多線程下共享變量的可見性

  下面的兩個(gè)例子演示了變量使用volatile和未使用volatile時(shí),變量更新對(duì)多線程執(zhí)行的影響。

  在VolatileDemo中,停止標(biāo)識(shí)stop使用volatile關(guān)鍵字修飾,初始值為false。

  創(chuàng)建子線程thread1并啟動(dòng),在子線程thread1任務(wù)中,當(dāng)不滿足停止條件時(shí),線程會(huì)一直運(yùn)行;當(dāng)滿足停止條件,終止任務(wù)。

  稍后,我們?cè)谥骶€程中設(shè)置停止標(biāo)識(shí)為true。執(zhí)行代碼,結(jié)果如下圖。

  我們可以看到在主線程設(shè)置stop=true后,子線程同時(shí)感知到stop的變化終止了任務(wù)。

  NonVolatileDemo中,停止標(biāo)識(shí)stop未使用volatile關(guān)鍵字修飾,初始值為false。其他代碼和VolatileDemo完全一致。

  執(zhí)行代碼,結(jié)果如下圖。

  我們可以看到在主線程設(shè)置stop=true后,子線程未及時(shí)感知到stop的變化,還在繼續(xù)執(zhí)行任務(wù)。

  可以看到,volatile關(guān)鍵字保證了多線程下共享變量的可見性,那到底什么是可見性問題呢?

  在單線程中,如果修改了一個(gè)變量的值,之后如果讀取這個(gè)值,讀取到的值肯定是最新值。

  但是在多線程環(huán)境下,如果在一個(gè)線程中修改了一個(gè)變量的值,之后其他的線程讀取這個(gè)值時(shí)有可能不能立即獲得這個(gè)變量的最新值。這就是可見性問題。

  那volatile是如何保證了可見性的呢?這我們就需要從硬件層面來了解可見性的本質(zhì)。

  從根源深度分析:volatile是怎么實(shí)現(xiàn)功能的?

  volatile基于內(nèi)存屏障實(shí)現(xiàn)可見性

  可見性問題主要由以下兩個(gè)現(xiàn)象引起的:

  多處理器能夠暫時(shí)在寄存器或本地緩沖區(qū)(如寫緩沖區(qū))中保存內(nèi)存中的值,這樣,不同處理器上的線程同時(shí)可能保存著同一個(gè)內(nèi)存位置的不同的值;

  為了使吞吐量最大化,編譯器和處理器在不會(huì)改變代碼語義的情況下,可以改變指令執(zhí)行的順序。

  以寫緩沖區(qū)為例,我們來看看它是如何影響數(shù)據(jù)的可見性的。

  現(xiàn)代計(jì)算機(jī)的核心硬件由CPU,內(nèi)存,磁盤和以及其他的IO設(shè)備組成。

  由于程序的代碼和數(shù)據(jù)都存儲(chǔ)在內(nèi)存中,計(jì)算機(jī)在完成一項(xiàng)任務(wù)的過程中,免不了要與內(nèi)存進(jìn)行交互,如讀取指令,讀取數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等。

  而現(xiàn)在內(nèi)存的IO操作速度遠(yuǎn)遠(yuǎn)跟不上CPU的運(yùn)算速度,如果內(nèi)存中的數(shù)據(jù)遲遲進(jìn)不了CPU,CPU就會(huì)一直處于等待狀態(tài)。

  為了彌補(bǔ)內(nèi)存速度的不足,人們?cè)诿總€(gè)處理器和內(nèi)存之間加入了盡可能接近處理器速度的高度緩存。

  高度緩存會(huì)將CPU運(yùn)算需要使用的數(shù)據(jù)從主內(nèi)存復(fù)制到緩存中,當(dāng)運(yùn)算結(jié)束后再將緩存同步回主內(nèi)存之中,這樣就提高了CPU訪問程序和數(shù)據(jù)的速度。

  在多核處理器系統(tǒng)中,每個(gè)處理器都有自己獨(dú)享的高速緩存,它們共享主內(nèi)存。高速緩存雖然解決了CPU和內(nèi)存速度不匹配的問題,但是這會(huì)帶來另外一個(gè)問題:

  當(dāng)兩個(gè)內(nèi)核同時(shí)使用同一數(shù)據(jù)時(shí),如果一方修改了數(shù)據(jù),未及時(shí)通知到對(duì)方,導(dǎo)致雙方緩存中持有的該數(shù)據(jù)的信息不一致,這就是緩存一致性問題。

  為了解決緩存一致性問題,保證每個(gè)處理器讀寫數(shù)據(jù)時(shí)都能得到最新的值,這就需要所有的處理器訪問緩存時(shí)都遵守一些協(xié)議,在讀寫數(shù)據(jù)時(shí)要根據(jù)協(xié)議來進(jìn)行操作。

  其中比較經(jīng)典,最出名的是Intel的MESI協(xié)議,MESI協(xié)議保證了每個(gè)處理器的緩存中使用的共享變量的副本是一致的。

  高速緩存是以緩存行(Cache line)為單位存儲(chǔ)的,Cache line是緩存和內(nèi)存進(jìn)行數(shù)據(jù)交換的最小單位。

  在MESI協(xié)議中,每個(gè)Cache line有四個(gè)狀態(tài),它們分別是M、E、S、I狀態(tài)。

  在MESI協(xié)議中,每個(gè)Cache不僅知道自己的讀寫操作,而且同時(shí)也監(jiān)聽其他Cache的讀寫操作。

  每個(gè)Cache line的狀態(tài)會(huì)根據(jù)本核和其他核的讀寫操作在4個(gè)狀態(tài)間進(jìn)行變換。

  MESI協(xié)議解決了緩存一致性問題,但是同時(shí)也帶來了一些問題。

  當(dāng)多個(gè) CPU 緩存持有相同的數(shù)據(jù)時(shí)(S 狀態(tài)),如果其中一個(gè) CPU 要對(duì)數(shù)據(jù)進(jìn)行修改,需要等待其他 CPU 將數(shù)據(jù)失效(I 狀態(tài)),那么這里就會(huì)有空等期(stall),這對(duì)于頻率很高的CPU來說,簡直不能接受!

  為了避免以上問題帶來的CPU資源浪費(fèi),現(xiàn)代的處理器引入了寫緩沖區(qū)。

  當(dāng)寫操作數(shù)據(jù)被提交時(shí),會(huì)把寫操作先放入 Store Buffer 中,同時(shí)向其他的CPU發(fā)送 invalidate 消息,隨后CPU可以去處理別的事情。

  當(dāng)收到其他所有 CPU 反饋的 invalidate acknowledge 消息時(shí),再將寫緩沖區(qū)中的數(shù)據(jù)寫入主存。

  對(duì)于同一個(gè) CPU 而言,在讀取 X 變量的時(shí)候,如若發(fā)現(xiàn) Store Buffer 中有尚未寫入到緩存的數(shù)據(jù) X,則直接從 Store Buffer 中讀取。

  這就保證了單線程中的代碼執(zhí)行順序,從CPU外部看該CPU一直在進(jìn)行讀寫操作, 而不必等待, 這極大地提高了CPU的效率。

  但是寫緩沖區(qū)會(huì)帶來另外一個(gè)問題,對(duì)于當(dāng)前CPU來說讀寫指令的執(zhí)行順序沒有變化。

  如下圖,對(duì)于本地CPU來說,指令的執(zhí)行順序還是先Store后Load,但是對(duì)于內(nèi)存和其他CPU來說,Load指令是先于Store指令執(zhí)行。

  這種情況可以認(rèn)為是一種指令重排序,而它會(huì)帶來內(nèi)存可見性問題。

  我們來看下面的例子。線程T1.T2共享變量x,y初始值為0.兩個(gè)線程T1.T2在兩個(gè)處理器內(nèi)核中分別執(zhí)行。T1在Core1中執(zhí)行,T2在Core2中執(zhí)行:

  在Core1 和Core2指令的執(zhí)行順序是在本地處理上不變的,但是由于寫緩沖區(qū)的引入,所以對(duì)于其他處理器存在以下的執(zhí)行順序和結(jié)果:輸出 (ry,rx)=(0.0)。

  重排序有可能會(huì)造成不可預(yù)知的問題,那有沒有什么辦法來禁止重排序呢?

  在計(jì)算機(jī)指令中,計(jì)算機(jī)提供了內(nèi)存屏障指令,用于禁止處理器的重排序。

  內(nèi)存屏障是一組CPU指令,它的作用是禁止重排序,強(qiáng)制數(shù)據(jù)訪問內(nèi)存的順序和程序代碼順序一樣。

  內(nèi)存屏障是一種標(biāo)準(zhǔn),不同的處理器廠商和操作系統(tǒng)可能會(huì)采用不同的實(shí)現(xiàn)。X86的內(nèi)存屏障指令包括讀屏障、寫屏障、全屏障。

  那Java開發(fā)時(shí),我們?nèi)绾蝺?nèi)存屏障呢?

  Java作為一種一次編寫,多處運(yùn)行的語言,開發(fā)者是不需要考慮平臺(tái)相關(guān)性的,同樣這個(gè)問題也不需要也不應(yīng)該由程序員來關(guān)心。

  在需要確保代碼執(zhí)行順序時(shí),JVM會(huì)在適當(dāng)位置插入一個(gè)內(nèi)存屏障命令,用來禁止特定類型的重排序。

  所以,Java虛擬機(jī)封裝了底層內(nèi)存屏障的實(shí)現(xiàn),提供了四種內(nèi)存屏障指令(在后面有說明),編譯器會(huì)根據(jù)底層的計(jì)算機(jī)架構(gòu),將內(nèi)存屏障替換為相應(yīng)的CPU指令。

  volatile 變量的原生實(shí)現(xiàn)

  volatile 變量就是是基于內(nèi)存屏障實(shí)現(xiàn)的。下面我們一起從源碼來剖析下volatile是如何實(shí)現(xiàn)了內(nèi)存可見性。

  步驟 0:下載源碼

  OpenJDK的源碼是托管在 Mercurial代碼版本管理平臺(tái)上的,可以使用Mercurial的代碼管理工具直接從遠(yuǎn)程倉庫(Repository)中下載獲取源碼。

  我們選用的項(xiàng)目是OpenJDK 8u,代碼遠(yuǎn)程倉庫地址:

  https://hg.openjdk.java.net/jdk8u/jdk8u/

  因?yàn)橄螺d源碼過程中需要執(zhí)行腳本文件get_source.sh,所以需要在Linux平臺(tái)下下載。

  大家可以在安裝了Linux環(huán)境的機(jī)器上下載,或者在自己的機(jī)器上安裝Linux虛擬機(jī)后進(jìn)行下載。

  獲取源代碼過程如下:

  OpenJDK 目錄結(jié)構(gòu)

  步驟 1:hsdis工具查看機(jī)器指令

  使用hsdis可以查看 Java 編譯后的機(jī)器指令。

  使用方法:把編譯好的 hsdis-amd64.dll放在 $JAVA_HOME/jre/bin/server 目錄下。

  就可以使用如下命令,查看程序的機(jī)器指令。

  在類VolatileFieldTest中屬性volatileField為volatile變量。

  在Linux中,執(zhí)行

  對(duì)于 volatile 修飾的變量進(jìn)行寫操作,在生成匯編代碼中,會(huì)有如下的指令:

  上面的操作就相當(dāng)于一個(gè)內(nèi)存屏障。

  lock指令:會(huì)使緊跟在其后的指令變成原子操作,lock指令是一個(gè)匯編層面的指令,作為前綴加在其他匯編指令之前,可以保證其后匯編操作的原子性。

  在多CPU環(huán)境中, LOCK指令可以確保一個(gè)處理器能獨(dú)占使用共享內(nèi)存。

  在計(jì)算機(jī)中每個(gè)CPU擁有自己的寄存器,緩沖區(qū)和高速緩存,所有CPU共享主內(nèi)存。

  如果是單核CPU環(huán)境,所有線程都會(huì)運(yùn)行相同CPU上,使用的是相同存儲(chǔ)空間不存在一致性問題,就不需要內(nèi)存屏障;

  如果是多核 CPU環(huán)境中,線程可能運(yùn)行在不同的CPU內(nèi)核上, 共享主內(nèi)存,就需用內(nèi)存屏障保障一致性。

  addl指令:加法操作指令。

  addl $0.0(%%esp)表示將數(shù)值0加到rsp寄存器中。esp寄存器指向棧頂?shù)膬?nèi)存單元,加上一個(gè)0.esp寄存器的數(shù)值依然不變。

  addl $0.0(%%esp) 使用此匯編指令再配合lock指令,實(shí)現(xiàn)了CPU的內(nèi)存屏障。

  步驟 2:volatile源碼解析

  下面我們通過OpenJDK源碼,來看看JVM是怎么實(shí)現(xiàn)volatile賦值的。

  查看volatile字段字節(jié)碼:ACC_VOLATILE

  通過javap命令javap -v -p VolatileFieldClass.class可以看到volatile的屬性的字節(jié)碼flags標(biāo)識(shí)中有個(gè)關(guān)鍵字ACC_VOLATILE。

  在不同的計(jì)算機(jī)架構(gòu)(操作系統(tǒng)和CPU架構(gòu))下,內(nèi)存屏障的實(shí)現(xiàn)也會(huì)不同,所以對(duì)應(yīng)的JVM的volatile底層實(shí)現(xiàn)在不同的平臺(tái)上也是不同的。

  下面我們以linux_x86為例,來一層層解開volatile的面紗。

  is_volatile方法:判斷一個(gè)變量是否是volatile類型

  通過關(guān)鍵字ACC_VOLATILE,我們可以定位到JVM源文件vm\utilities\accessFlags.hpp文件,代碼如下:

  可以看到is_volatile函數(shù),這個(gè)函數(shù)是判斷變量字節(jié)碼中是否有 ACC_VOLATILE這個(gè)flag。

  Java中volatile變量賦值:C++實(shí)現(xiàn)

  Java字節(jié)碼是通過BytecodeInterpreter解釋器來執(zhí)行,我們?cè)赽ytecodeInterpreter.cpp文件根據(jù)關(guān)鍵字is_volatile搜索,可以看到如下代碼:

  在這段代碼中,cache->is_volatile()這段代碼,cache代表變量在常量池緩存中的實(shí)例(本例中為volatileField),作用是判斷變量是否被 volatile修飾。

  接著,根據(jù)當(dāng)前變量的類型來賦值,會(huì)先判斷volatile變量類型(tos_type變量),后面有不同的基礎(chǔ)類型的調(diào)用,比如int類型就調(diào)用release_int_field_put。

  release_int_field_put這個(gè)方法的實(shí)現(xiàn)在文件oop.inline.hpp中:

  賦值動(dòng)作int_field_addr外面包裝執(zhí)行了OrderAccess::release_store方法。

  我們看看 OrderAccess::release_store做了什么,

  它的定義在 :

  vm\runtime\orderAccess.hpp中,

  linux_x86中實(shí)現(xiàn)在:

  os_cpu\linux_x86\vm\orderAccess_linux_x86.inline.hpp

  可以看到volatile操作,第一步是實(shí)現(xiàn)了C++的volatile變量的原生賦值實(shí)現(xiàn)。

  C/C++中的volatile關(guān)鍵字,用來修飾變量,表明變量可能會(huì)通過某種方式發(fā)生改變。

  被volatile聲明的變量,會(huì)告訴編譯器與該變量有關(guān)的運(yùn)算,不要進(jìn)行編譯優(yōu)化,且變量的值都必須直接寫入內(nèi)存地址或從內(nèi)存地址讀取。

  volatile使用內(nèi)存屏障禁止重排序

  賦值操作完成以后,我們可以看到最后一行執(zhí)行的語句是:

  這是JVM的storeload一個(gè)內(nèi)存屏障。

  JMM 把內(nèi)存屏障指令分為了四類,可以在vm\runtime\orderAccess.hpp找到對(duì)應(yīng)的實(shí)現(xiàn)方法:

  內(nèi)存屏障的解釋也可以在orderAccess.hpp該文件中看到:

  其中StoreLoad Barriers 是一個(gè)“全能型”的屏障,它同時(shí)具有其他三個(gè)屏障的效果。

  現(xiàn)代的多處理器大都支持該屏障,執(zhí)行該屏障開銷會(huì)很昂貴。

  以linux_x86為例,我們可以在:

  os_cpu\linux_x86\vm\orderAccess_linux_x86.inline.hpp看到它們的實(shí)現(xiàn):

  當(dāng)調(diào)用storeload屏障時(shí),會(huì)調(diào)用fence()方法:

  在上面的代碼中,我們看到了熟悉的匯編指令 lock; addl $0.0(%%esp):

  os::is_MP(),會(huì)判斷當(dāng)前環(huán)境是否是多核。單核不存在一致性問題,多核CPU才需要使用內(nèi)存屏障。

  cc代表的是寄存器,memory代表是內(nèi)存。

  同時(shí)使用“cc”和“memory”,會(huì)通知編譯器和處理器volatile變量在內(nèi)存或者寄存器內(nèi)的值已經(jīng)發(fā)生了修改,要重新加載,需要直接從主內(nèi)存中讀取。

  那我們平時(shí)在使用volatile時(shí)應(yīng)該遵循什么原則呢?

  針對(duì)不同場景,關(guān)于volatile的使用建議

  正確用法

  使用 volatile 變量的主要原因是單個(gè)字段同步操作的簡易性。

  如果只使用了volatile就能實(shí)現(xiàn)線程安全,那就放心的使用它,如果同時(shí)還需要添加其他的同步措施,那就不要使用。

  正確使用的場景舉例:變量本身標(biāo)識(shí)是一種狀態(tài),或者引用變量的某些屬性狀態(tài),在代碼中需要確保這些狀態(tài)的可見性,這時(shí)就可使用volatile。

  volatile 變量僅僅是一個(gè)狀態(tài)標(biāo)識(shí),用于指示發(fā)生了一個(gè)重要的一次性事件,例如完成初始化標(biāo)識(shí)或請(qǐng)求終止標(biāo)識(shí)。

  這樣只要任何一個(gè)線程調(diào)用了shutdown(),其他線程在執(zhí)行doWork時(shí)都可以立即感知到stop變量的變化,這時(shí)就可以大膽的使用volatile。

  這種類型的狀態(tài)標(biāo)記的一個(gè)公共特性是:通常只有一種狀態(tài)轉(zhuǎn)換,如標(biāo)志從false 轉(zhuǎn)換為true。

  這時(shí)使用volatile要比synchronized要簡單有效的多,如果使用synchronized還會(huì)影響系統(tǒng)的吞吐量。

  錯(cuò)誤用法

  我們知道基本數(shù)據(jù)類型的單次讀、寫操作時(shí)具有原子性。同樣單個(gè)volatile 變量單次的讀、寫操作也具有原子性。

  但是對(duì)于類似于 ++,--,邏輯非!這類復(fù)合操作,這些操作整體上是不具有原子性的。

  如下面例子:

  造成這種情況的原因是因?yàn)?+操作分三次操作完成的。

  我們執(zhí)行反編譯命令:

  javap -c VolatileTest.class,

  可以看到increase()函數(shù)中race++是由以下字節(jié)碼指令構(gòu)成:

  字節(jié)碼釋義如下:

  從字節(jié)碼層面很容易分析出來并發(fā)失敗的原因了。

  假如有兩條線程同時(shí)執(zhí)行race++:

  線程A,線程B同時(shí)執(zhí)行g(shù)etfield指令把race的值壓入各自的操作棧頂時(shí),volatile關(guān)鍵字可以保證來race的值在此時(shí)是正確(最新的值)的;

  線程A依次執(zhí)行完了后續(xù)操作iadd和putfield,此時(shí)主內(nèi)存中race的值已被增大1;

  線程A執(zhí)行完畢后,線程B操作棧頂?shù)膔ace值就變成了過期的數(shù)據(jù),這時(shí)線程B執(zhí)行iadd、putfield后就會(huì)把較小的值同步會(huì)主內(nèi)存了。

  在這種場景中,我們?nèi)匀灰ㄟ^加鎖來保證原子性,此時(shí)就不建議使用volatile。

  以下為正確實(shí)現(xiàn),使用synchronized保證 race++操作的原子性。

  到此,我們徹底的揭開了volatile 的面紗。

  現(xiàn)在我們明白了 volatile 保證有序性和可見性的原理,也知道了使用時(shí)應(yīng)遵循的原則。

  希望大家不虛此行,可以在日后的工作中能熟練的運(yùn)用 volatile 關(guān)鍵字。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
volatile關(guān)鍵字的作用
分布式并發(fā)編程,線程安全性,原理分析
JMM和底層實(shí)現(xiàn)原理
我們所熟悉的java并發(fā)volatile!
一個(gè)volatile跟面試官扯了半個(gè)小時(shí)
Java并發(fā)JVM內(nèi)存模型處理器從而實(shí)現(xiàn)安全且高效的多線程功能
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服