自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java并發(fā)編程-內(nèi)存模型及volatile

存儲 存儲軟件
大家都知道,計算機在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。這時就存在一個問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程則慢得多(不是一個數(shù)量級),因此如果任何時候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進行,會大大降低指令執(zhí)行的速度。因此在CPU里面就有了高速緩存。

 內(nèi)存模型相關概念

大家都知道,計算機在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。這時就存在一個問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程則慢得多(不是一個數(shù)量級),因此如果任何時候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進行,會大大降低指令執(zhí)行的速度。因此在CPU里面就有了高速緩存。

也就是說,當程序在運行過程中,會將運算需要的數(shù)據(jù)從主內(nèi)存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當運算結束之后,再將高速緩存中的數(shù)據(jù)刷新到主內(nèi)存當中。

[[252831]]

舉個簡單的例子,比如下面的這段代碼:

  1. i = i + 1;1 

當線程執(zhí)行這個語句時,會先從主存當中讀取i的值,然后復制一份到高速緩存當中,然后CPU執(zhí)行指令對i進行加1操作,然后將數(shù)據(jù)寫入高速緩存,***將高速緩存中i***的值刷新到主存當中。

這個代碼在單線程中運行是沒有任何問題的,但是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行于不同的CPU中,因此每個線程運行時有自己的高速緩存。比如同時有2個線程執(zhí)行這段代碼,假如初始時i的值為0,那么我們希望兩個線程執(zhí)行完之后i的值變?yōu)?。但可能存在下面一種情況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,然后線程1進行加1操作,然后把i的***值1寫入到內(nèi)存。此時線程2的高速緩存當中i的值還是0,進行加1操作之后,i的值為1,然后線程2把i的值寫入內(nèi)存。最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。也就是說,如果一個變量在多個CPU中都存在緩存(一般在多線程編程時才會出現(xiàn)),那么就可能存在緩存不一致的問題。

解決方法:

  • 通過在總線加LOCK鎖的方式
  • 通過緩存一致性協(xié)議

在早期的CPU中,都是通過LOCK鎖的方式來實現(xiàn)的。因為CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK鎖的話,會阻塞其他CPU對其他部件訪問(如內(nèi)存),從而使得只有一個CPU能使用這個變量的內(nèi)存。比如上面例子中如果一個線程在執(zhí)行 i = i +1,如果在執(zhí)行這段代碼的過程中,在總線上發(fā)出了LCOK鎖的信號,那么只有等待這段代碼執(zhí)行完畢之后,其他CPU才能從變量i所在的內(nèi)存讀取變量,然后進行相應的操作。這樣就解決了緩存不一致的問題。

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

所以就出現(xiàn)了緩存一致性協(xié)議。最出名的就是Intel的MESI協(xié)議,MESI協(xié)議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數(shù)據(jù)時,如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發(fā)出信號通知其他CPU將該變量的緩存行置為無效狀態(tài),因此當其他CPU需要讀取這個變量時,發(fā)現(xiàn)自己緩存中該變量的緩存行是無效的,那么它就會從內(nèi)存重新讀取。

Java內(nèi)存區(qū)域與Java內(nèi)存模型

Java內(nèi)存區(qū)域

我們在《深入理解Java虛擬機》第2章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常一文中已經(jīng)詳細講解過Java內(nèi)存區(qū)域,現(xiàn)在我們再簡單歸納一下。

方法區(qū)(Method Area)

方法區(qū)屬于線程共享的內(nèi)存區(qū)域,又稱Non-Heap(非堆),主要用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),根據(jù)Java 虛擬機規(guī)范的規(guī)定,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError 異常。值得注意的是在方法區(qū)中存在一個叫運行時常量池(Runtime Constant Pool)的區(qū)域,它主要用于存放編譯器生成的各種字面量和符號引用,這些內(nèi)容將在類加載后存放到運行時常量池中,以便后續(xù)使用。

JVM堆(Java Heap)

Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機啟動時創(chuàng)建,是Java 虛擬機所管理的內(nèi)存中***的一塊,主要用于存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做GC 堆,如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

程序計數(shù)器(Program Counter Register)

屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當前線程所執(zhí)行的字節(jié)碼行號指示器。字節(jié)碼解釋器工作時,通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等基礎功能都需要依賴這個計數(shù)器來完成。

虛擬機棧(Java Virtual Machine Stacks)

屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時創(chuàng)建,總數(shù)與線程關聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。每個方法執(zhí)行時都會創(chuàng)建一個棧楨來存儲方法的的變量表、操作數(shù)棧、動態(tài)鏈接方法、返回值、返回地址等信息。每個方法從調(diào)用直結束就對于一個棧楨在虛擬機棧中的入棧和出棧過程,如下:

本地方法棧(Native Method Stacks)

本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機用到的 Native 方法相關,一般情況下,我們無需關心此區(qū)域。

Java內(nèi)存模型

Java內(nèi)存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實存在,它描述的是一組規(guī)則或規(guī)范,主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)。

JMM與Java內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當說JMM描述的是一組規(guī)則,通過這組規(guī)則控制程序中各個變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問方式,JMM是圍繞原子性、有序性、可見性展開的。JMM與Java內(nèi)存區(qū)域唯一相似點,都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應該包括了堆和方法區(qū),而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應該包括程序計數(shù)器、虛擬機棧以及本地方法棧?;蛟S在某些地方,我們可能會看見主內(nèi)存被描述為堆內(nèi)存,工作內(nèi)存被稱為線程棧,實際上他們表達的都是同一個含義。關于JMM中的主內(nèi)存和工作內(nèi)存說明如下:

主內(nèi)存

主要存儲的是Java實例對象,所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的局部變量,當然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個變量進行訪問可能會發(fā)現(xiàn)線程安全問題。

工作內(nèi)存

主要存儲當前方法的所有本地變量信息(工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝),每個線程只能訪問自己的工作內(nèi)存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執(zhí)行的是同一段代碼,它們也會各自在自己的工作內(nèi)存中創(chuàng)建屬于當前線程的本地變量,當然也包括了字節(jié)碼行號指示器、相關Native方法的信息。注意由于工作內(nèi)存是每個線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。

JMM內(nèi)存交互

關于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細節(jié),Java內(nèi)存模型中定義了以下8種操作來完成,虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的、不可再分的。

lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標識為一條線程獨占的狀態(tài)。

unlock(解鎖):作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。

read(讀取):作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用。

load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。

use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。

assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。

store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存 中,以便隨后的write操作使用。

write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

如果要把一個變量從主內(nèi)存復制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行store和write操作。注意,Java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說,read與load之間、store與write之間是可插入其他指令的,如對主內(nèi)存中的變量a、b進行訪問時,一種可能出現(xiàn)順序是read a、read b、load b、load a。

原子性、可見性與有序性

在并發(fā)編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個概念:

原子性

原子性:即一個操作或者多個操作,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

舉個最簡單的例子,大家想一下假如為一個32位的變量賦值過程不具備原子性的話,會發(fā)生什么后果?

  1. i = 9;1 

假若一個線程執(zhí)行到這個語句時,我暫且假設為一個32位的變量賦值包括兩個過程:為低16位賦值,為高16位賦值。那么就可能發(fā)生一種情況:當將低16位數(shù)值寫入之后,突然被中斷,而此時又有一個線程去讀取i的值,那么讀取到的就是錯誤的數(shù)據(jù)。

來看看下面語句:

  1. x = 10;        //語句1 
  2. y = x;         //語句2 
  3. x++;           //語句3 
  4. x = x + 1;     //語句41234 

只有語句1是原子性操作,其他三個語句都不是原子性操作。

語句2實際上包含2個操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存,雖然讀取x的值以及將x的值寫入工作內(nèi)存這2個操作都是原子性操作,但是合起來就不是原子性操作了。

同樣的,x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值。

也就是說,只有簡單的讀取、賦值(而且必須是將數(shù)字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。

可見性

可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

舉個簡單的例子,看下面這段代碼:

//線程1執(zhí)行的代碼

  1. int i = 0; 
  2. i = 10; 

//線程2執(zhí)行的代碼

  1. j = i;123456 

由上面的分析可知,當線程1執(zhí)行 i =10這句時,會先把i的初始值加載到線程1的工作內(nèi)存中,然后賦值為10,那么線程1工作內(nèi)存中i的值變?yōu)?0了,卻沒有立即寫入到主內(nèi)存當中。此時線程2執(zhí)行 j = i,它會先去主內(nèi)存讀取i的值并加載到線程2的工作內(nèi)存,注意此時主內(nèi)存當中i的值還是0,那么就會使得j的值為0,而不是10。這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。

有序性

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結果和代碼順序執(zhí)行的結果是一致的。

舉個簡單的例子,看下面這段代碼:

  1. int i = 0;               
  2. boolean flag = false
  3. i = 1;                //語句1   
  4. flag = true;          //語句21234 

上面代碼定義了一個int型變量,定義了一個boolean類型變量,然后分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。

但是要注意,雖然處理器會對指令進行重排序,但是它會保證程序最終結果會和代碼順序執(zhí)行結果相同,那么它靠什么保證的呢?再看下面一個例子:

  1. int a = 10;    //語句1 
  2. int r = 2;     //語句2 
  3. a = a + 3;     //語句3 
  4. r = a*a;       //語句41234 

這段代碼有4個語句,那么可能的一個執(zhí)行順序是:2->1->3->4

那么可不可能是這個執(zhí)行順序呢:2->1->4->3 ?

不可能,因為處理器在進行重排序時是會考慮指令之間的數(shù)據(jù)依賴性,如果一個指令Instruction 2必須用到Instruction 1的結果,那么處理器會保證Instruction 1會在Instruction 2之前執(zhí)行。

雖然重排序不會影響單個線程內(nèi)程序執(zhí)行的結果,但是多線程呢?下面看一個例子:

  1. //線程1: 
  2. context = loadContext();   //語句1 
  3. inited = true;             //語句2 
  4.  
  5. //線程2: 
  6. while(!inited ){ 
  7.   sleep() 
  8. doSomethingwithconfig(context);123456789 

上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成,那么就會跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時context并沒有被初始化,就會導致程序出錯。

從上面可以看出,指令重排序不會影響單個線程的執(zhí)行,但是會影響到線程并發(fā)執(zhí)行的正確性。

備注:

指令重排序,一般分以下3種:

編譯器優(yōu)化的重排序

編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。

指令并行的重排序

現(xiàn)代處理器采用了指令級并行技術來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個執(zhí)行的語句無需依賴前面執(zhí)行的語句的結果),處理器可以改變語句對應的機器指令的執(zhí)行順序

內(nèi)存系統(tǒng)的重排序

由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載(load)和存儲(store)操作看上去可能是在亂序執(zhí)行,因為三級緩存的存在,導致內(nèi)存與緩存的數(shù)據(jù)同步存在時間差。

從java源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷下面三種重排序:

上述的1屬于編譯器重排序,2和3屬于處理器重排序。

JMM的解決方案

那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問規(guī)則,往大一點說是定義了程序執(zhí)行的次序。注意,為了獲得較好的執(zhí)行性能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來提升指令執(zhí)行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內(nèi)存模型中,也會存在緩存一致性問題和指令重排序的問題。

那么Java語言本身對原子性、可見性以及有序性提供了哪些保證呢?

原子性

上面講過了JMM內(nèi)存交互的8種指令,這些指令全部都是原子性的操作。

Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作,如果要實現(xiàn)更大范圍操作的原子性,可以通過synchronized和Lock來實現(xiàn)。由于synchronized和Lock能夠保證任一時刻只有一個線程執(zhí)行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。

可見性

對于可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。

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

有序性

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

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

happens-before 原則

倘若在程序開發(fā)中,僅靠sychronized和volatile關鍵字來保證原子性、可見性以及有序性,那么編寫并發(fā)程序可能會顯得十分麻煩,幸運的是,在Java內(nèi)存模型中,還提供了happens-before 原則來輔助保證程序執(zhí)行的原子性、可見性以及有序性的問題,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的依據(jù),happens-before 原則內(nèi)容如下:

程序順序原則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。

鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作。

volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。

傳遞規(guī)則:A先于B ,B先于C,那么A必然先于C。

線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作。

線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。

線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行。

對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始。

上述8條原則無需手動添加任何同步手段(synchronized|volatile)即可達到效果。

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

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

(1)***條規(guī)則我的理解就是一段程序代碼的執(zhí)行在單個線程中看起來是有序的。這個規(guī)則是用來保證程序在單線程中執(zhí)行結果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。

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

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

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

深入剖析volatile關鍵字

在前面講述了很多東西,其實都是為講述volatile關鍵字作鋪墊,那么接下來我們就進入主題。

volatile語義

一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

禁止進行指令重排序。

先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:

  1. //線程1 
  2. boolean stop = false
  3. while(!stop){ 
  4.     doSomething(); 
  5.  
  6. //線程2 
  7. stop = true;12345678 

這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數(shù)時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。

下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當中。那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。

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

***:使用volatile關鍵字會強制將修改的值立即寫入主存;

第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

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

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

volatile保證原子性嗎?

從上面知道volatile關鍵字保證了操作的可見性,但是volatile能保證對變量的操作是原子性嗎?

  1. public class Test { 
  2.     public volatile int inc = 0; 
  3.  
  4.     public void increase() { 
  5.         inc++; 
  6.     } 
  7.  
  8.     public static void main(String[] args) { 
  9.         final Test test = new Test(); 
  10.         for(int i=0;i<10;i++){ 
  11.             new Thread(){ 
  12.                 public void run() { 
  13.                     for(int j=0;j<1000;j++) 
  14.                         test.increase(); 
  15.                 }; 
  16.             }.start(); 
  17.         } 
  18.  
  19.         while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完 
  20.             Thread.yield(); 
  21.         System.out.println(test.inc); 
  22.     } 
  23. }1234567891011121314151617181920212223 

大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發(fā)現(xiàn)每次運行結果都不一致,都是一個小于10000的數(shù)字。

可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應該是1000*10=10000。

這里面就有一個誤區(qū)了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性??梢娦灾荒鼙WC每次讀取的是***的值,但是volatile沒辦法保證對變量的操作的原子性。

在前面已經(jīng)提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內(nèi)存。就有可能導致下面這種情況出現(xiàn):假如某個時刻變量inc的值為10,線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時10,然后進行加1操作,并把11寫入工作內(nèi)存,***寫入主存。然后線程1接著進行加1操作,由于已經(jīng)讀取了inc的值,注意此時在線程1的工作內(nèi)存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,***寫入主存。那么兩個線程分別進行了一次自增操作后,inc只增加了1。

解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規(guī)則中的volatile變量規(guī)則,但是要注意,線程1對變量進行讀取操作之后,被阻塞了的話,并沒有對inc值進行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒有進行修改,所以線程2根本就不會看到修改的值。

根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。

把上面的代碼改成以下任何一種都可以達到效果:

采用synchronized:

  1. public class Test { 
  2.     public  int inc = 0; 
  3.  
  4.     public synchronized void increase() { 
  5.         inc++; 
  6.     } 
  7.  
  8.     public static void main(String[] args) { 
  9.         final Test test = new Test(); 
  10.         for(int i=0;i<10;i++){ 
  11.             new Thread(){ 
  12.                 public void run() { 
  13.                     for(int j=0;j<1000;j++) 
  14.                         test.increase(); 
  15.                 }; 
  16.             }.start(); 
  17.         } 
  18.  
  19.         while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完 
  20.             Thread.yield(); 
  21.         System.out.println(test.inc); 
  22.     } 
  23. }1234567891011121314151617181920212223 
  24.  
  25. 采用Lock: 
  26.  
  27.  
  28.  
  29. public class Test { 
  30.     public  int inc = 0; 
  31.     Lock lock = new ReentrantLock(); 
  32.  
  33.     public  void increase() { 
  34.         lock.lock(); 
  35.         try { 
  36.             inc++; 
  37.         } finally{ 
  38.             lock.unlock(); 
  39.         } 
  40.     } 
  41.  
  42.     public static void main(String[] args) { 
  43.         final Test test = new Test(); 
  44.         for(int i=0;i<10;i++){ 
  45.             new Thread(){ 
  46.                 public void run() { 
  47.                     for(int j=0;j<1000;j++) 
  48.                         test.increase(); 
  49.                 }; 
  50.             }.start(); 
  51.         } 
  52.  
  53.         while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完 
  54.             Thread.yield(); 
  55.         System.out.println(test.inc); 
  56.     } 
  57. }1234567891011121314151617181920212223242526272829 

采用Lock:

  1. public class Test { 
  2.     public AtomicInteger inc = new AtomicInteger(); 
  3.  
  4.     public void increase() { 
  5.         inc.getAndIncrement(); 
  6.     } 
  7.  
  8.     public static void main(String[] args) { 
  9.         final Test test = new Test(); 
  10.         for(int i=0;i<10;i++){ 
  11.             new Thread(){ 
  12.                 public void run() { 
  13.                     for(int j=0;j<1000;j++) 
  14.                         test.increase(); 
  15.                 }; 
  16.             }.start(); 
  17.         } 
  18.  
  19.         while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完 
  20.             Thread.yield(); 
  21.         System.out.println(test.inc); 
  22.     } 
  23. }1234567891011121314151617181920212223 

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數(shù)),減法操作(減一個數(shù))進行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實現(xiàn)原子性操作的(Compare And Swap),CAS實際上是利用處理器提供的CMPXCHG指令實現(xiàn)的,而處理器執(zhí)行CMPXCHG指令是一個原子性操作。

volatile能保證有序性嗎?

在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

volatile關鍵字禁止指令重排序有兩層意思:

當程序執(zhí)行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行,且結果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行;

在進行指令優(yōu)化時,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。

舉個簡單的例子:

  1. //x、y為非volatile變量 
  2. //flag為volatile變量 
  3.  
  4. x = 2;        //語句1 
  5. y = 0;        //語句2 
  6. flag = true;  //語句3 
  7. x = 4;         //語句4 
  8. y = -1;       //語句512345678 

由于flag變量為volatile變量,那么在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

并且volatile關鍵字能保證,執(zhí)行到語句3時,語句1和語句2必定是執(zhí)行完畢了的,且語句1和語句2的執(zhí)行結果對語句3、語句4、語句5是可見的。

那么我們回到前面舉的一個例子:

  1. //線程1: 
  2. context = loadContext();   //語句1 
  3. inited = true;             //語句2 
  4.  
  5. //線程2: 
  6. while(!inited ){ 
  7.   sleep() 
  8. doSomethingwithconfig(context);123456789 

前面舉這個例子的時候,提到有可能語句2會在語句1之前執(zhí)行,那么久可能導致context還沒被初始化,而線程2中就使用未初始化的context去進行操作,導致程序出錯。

這里如果用volatile關鍵字對inited變量進行修飾,就不會出現(xiàn)這種問題了,因為當執(zhí)行到語句2時,必定能保證context已經(jīng)初始化完畢。

volatile的原理和實現(xiàn)機制

下面這段話摘自《深入理解Java虛擬機》:

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關鍵字時,會多出一個lock前綴指令”

lock前綴指令實際上相當于一個內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會提供3個功能:

它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成;

它會強制將對緩存的修改操作立即寫入主存;

如果是寫操作,它會導致其他CPU中對應的緩存行無效。

使用volatile關鍵字的場景

synchronized關鍵字是防止多個線程同時執(zhí)行一段代碼,那么就會很影響程序執(zhí)行效率,而volatile關鍵字在某些情況下性能要優(yōu)于synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:

對變量的寫操作不依賴于當前值

該變量沒有包含在具有其他變量的不變式中

實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態(tài),包括變量的當前狀態(tài)。

事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程序在并發(fā)時能夠正確執(zhí)行。

下面列舉幾個Java中使用volatile的幾個場景:

1.狀態(tài)標記量

  1. volatile boolean flag = false
  2.  
  3. while(!flag){ 
  4.     doSomething(); 
  5.  
  6. public void setFlag() { 
  7.     flag = true
  8. }123456789 
  9.  
  10. volatile boolean inited = false
  11. //線程1: 
  12. context = loadContext();   
  13. inited = true;             
  14.  
  15. //線程2: 
  16. while(!inited ){ 
  17. sleep() 
  18. doSomethingwithconfig(context);12345678910 

2.double check

  1. class Singleton{ 
  2.     private volatile static Singleton instance = null
  3.  
  4.     private Singleton() { 
  5.  
  6.     } 
  7.  
  8.     public static Singleton getInstance() { 
  9.         if(instance==null) { 
  10.             synchronized (Singleton.class) { 
  11.                 if(instance==null
  12.                     instance = new Singleton(); 
  13.             } 
  14.         } 
  15.         return instance; 
  16.     } 

 

責任編輯:武曉燕 來源: 架構師成長營
相關推薦

2023-07-11 08:43:43

volatileJava內(nèi)存

2016-09-26 17:09:28

Java并發(fā)編程內(nèi)存模型

2013-08-07 10:46:07

Java并發(fā)編程

2020-11-11 08:45:48

Java

2024-11-27 09:26:29

2025-04-25 08:00:00

volatileJava編程

2023-10-27 07:47:58

Java語言順序性

2016-09-19 21:53:30

Java并發(fā)編程解析volatile

2023-06-26 08:02:34

JSR重排序volatile

2021-07-06 14:47:30

Go 開發(fā)技術

2023-10-27 07:47:37

計算機內(nèi)存模型

2024-01-29 10:34:37

Java編程

2021-07-30 13:35:43

共享內(nèi)存 Actor

2017-09-19 14:53:37

Java并發(fā)編程并發(fā)代碼設計

2022-07-10 20:49:57

javaVolatile線程

2011-12-29 13:31:15

Java

2025-02-19 00:05:18

Java并發(fā)編程

2025-02-17 00:00:25

Java并發(fā)編程

2022-05-05 07:38:32

volatilJava并發(fā)

2018-07-04 14:43:55

對象模型內(nèi)存結構內(nèi)存模型
點贊
收藏

51CTO技術棧公眾號