多線程第二節_jmm與volitile

1. 什麼是JMM模型

1.1 JMM不同於jvm內存區域模型

	java內存模型(java memory model)是一種抽象概念,並不真實存在,描述的是一組規則和規範,通過這組規範定義了程序中各個變量(包括實例字段,靜態字段 和構成數組對象的元素)的訪問方式。JVM運行程序的實體是線程,而每個線程創建時 JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),用於存儲線程私有的數據,而Java 內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問, 但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝的自 己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作 主內存中的變量,工作內存中存儲着主內存中的變量副本拷貝,前面說過,工作內存是每個 線程的私有數據區域,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必 須通過主內存來完成。
	jmm與jvm的區別
    JMM與JVM內存區域的劃分是不同的概念層次,更恰當說JMM描述的是一組規則,通 過這組規則控制程序中各個變量在共享數據區域和私有數據區域的訪問方式,JMM是圍繞 原子性,有序性、可見性展開。JMM與Java內存區域唯一相似點,都存在共享數據區域和 私有數據區域,在JMM中主內存屬於共享數據區域,從某個程度上講應該包括了堆和方法 區,而工作內存數據線程私有數據區域,從某個程度上講則應該包括程序計數器、虛擬機棧 以及本地方法棧。

1655196433296

2. JMM存在的必要性

3. 數據同步八大原子操作

	數據同步八大原子操作 
        (1)lock(鎖定):作用於主內存的變量,把一個變量標記爲一條線程獨佔狀態 (2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後 的變量纔可以被其他線程鎖定 (3)read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存 中,以便隨後的load動作使用 (4)load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工 作內存的變量副本中 (5)use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎 (6)assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內 存的變量 (7)store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存 中,以便隨後的write的操作 (8)write(寫入):作用於工作內存的變量,它把store操作從工作內存中的一個變量的值 傳送到主內存的變量中 如果要把一個變量從主內存中複製到工作內存中,就需要按順序地執行read和load操 作,如果把變量從工作內存中同步到主內存中,就需要按順序地執行store和write操作。但
Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

1655196626561

3.1 同步規則分析

	同步規則分析 1)不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內 存中2)一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化 (load或者assign)的變量。即就是對一個變量實施use和store操作之前,必須先自行 assign和load操作。 3)一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重 復執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。lock 和unlock必須成對出現。 4)如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個 變量之前需要重新執行load或assign操作初始化變量的值。 5)如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去 unlock一個被其他線程鎖定的變量。 6)對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write 操作)

4 併發編程的可見性,原子性與有序性問題

	原子性
        原子性指的是一個操作是不可中斷的,即使是在多線程環境下,一個操作一旦開始就不 會被其他線程影響。 在java中,對基本數據類型的變量的讀取和賦值操作是原子性操作有點要注意的是,對 於32位系統的來說,long類型數據和double類型數據(對於基本數據類型,
byte,short,int,float,boolean,char讀寫是原子操作),它們的讀寫並非原子性的,也就是說 如果存在兩條線程同時對long類型或者double類型的數據進行讀寫是存在相互干擾的,因 爲對於32位虛擬機來說,每次原子讀寫是32位的,而long和double則是64位的存儲單元, 這樣會導致一個線程在寫時,操作完前32位的原子操作後,輪到B線程讀取時,恰好只讀取 到了後32位的數據,這樣可能會讀取到一個既非原值又不是線程修改值的變量,它可能 是“半個變量”的數值,即64位數據被兩個線程分成了兩次讀取。但也不必太擔心,因爲 讀取到“半個變量”的情況比較少見,至少在目前的商用的虛擬機中,幾乎都把64位的數 據的讀寫操作作爲原子操作來執行,因此對於這個問題不必太在意,知道這麼回事即可。
	可見性
        理解了指令重排現象後,可見性容易了,可見性指的是當一個線程修改了某個共享變量 的值,其他線程是否能夠馬上得知這個修改的值。對於串行程序來說,可見性是不存在的, 因爲我們在任何一個操作中修改了某個變量的值,後續的操作中都能讀取這個變量值,並且 是修改過的新值。 但在多線程環境中可就不一定了,前面我們分析過,由於線程對共享變量的操作都是線 程拷貝到各自的工作內存進行操作後才寫回到主內存中的,這就可能存在一個線程A修改了 共享變量x的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量x進行操 作,但此時A線程工作內存中共享變量x對線程B來說並不可見,這種工作內存與主內存同步 延遲現象就造成了可見性問題,另外指令重排以及編譯器優化也可能導致可見性問題,通過 前面的分析,我們知道無論是編譯器優化還是處理器優化的重排現象,在多線程環境下,確 實會導致程序輪序執行的問題,從而也就導致可見性問題。
	有序性
        有序性是指對於單線程的執行代碼,我們總是認爲代碼的執行是按順序依次執行的,這 樣的理解並沒有毛病,畢竟對於單線程而言確實如此,但對於多線程環境,則可能出現亂序 現象,因爲程序編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順 序未必一致,要明白的是,在Java程序中,倘若在本線程內,所有操作都視爲有序行爲,如 果是多線程環境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單 線程內保證串行語義執行的一致性,後半句則指指令重排現象和工作內存與主內存同步延遲 現象。

5 JMM如何解決 原子、可見、有序性問題

	原子性問題 
        除了JVM自身提供的對基本數據類型讀寫操作的原子性外,可以通過 synchronized和 Lock實現原子性。因爲synchronized和Lock能夠保證任一時刻只有一個線程訪問該代碼 塊
	可見性問題 
        volatile關鍵字保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值立即 被其他的線程看到,即修改的值立即更新到主存中,當其他線程需要讀取時,它會去內存中 讀取新值。synchronized和Lock也可以保證可見性,因爲它們可以保證任一時刻只有一個 線程能訪問共享資源,並在其釋放鎖之前將修改的變量刷新到內存中
	有序性問題 
        在Java裏面,可以通過volatile關鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性,很顯然, synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行 同步代碼,自然就保證了有序性
	Java內存模型:
        每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變 量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪 問其他線程的工作內存。Java內存模型具備一些先天的“有序性”,即不需要通過任何手段 就能夠得到保證的有序性,這個通常也稱爲happens-before 原則。如果兩個操作的執行次 序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以 隨意地對它們進行重排序
	指令重排序:
        java語言規範規定JVM線程內部維持順序化語義。即只要程序的最終結果 與它順序化情況的結果相等,那麼指令的執行順序可以與代碼順序不一致,此過程叫指令的 重排序。指令重排序的意義是什麼?JVM能根據處理器特性(CPU多級緩存系統、多核處 理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的 發揮機器性能。

1655196882914

	as-if-serial語義 
        as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器爲了提高並行度),(單 線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語 義。爲了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序, 因爲這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作就可 能被編譯器和處理器重排序。
	happens-before 原則
	只靠sychronized和volatile關鍵字來保證原子性、可見性以及有序性,那麼編寫併發 程序可能會顯得十分麻煩,幸運的是,從JDK 5開始,Java使用新的JSR-133內存模型,提 供了happens-before 原則來輔助保證程序執行的原子性、可見性以及有序性的問題,它是 判斷數據是否存在競爭、線程是否安全的依據,happens-before 原則內容如下 1. 程序順序原則,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執 行。2. 鎖規則 解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是 說,如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個 鎖)。 3. volatile規則 volatile變量的寫,先發生於讀,這保證了volatile變量的可見性,簡 單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的 值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的 線程總是能夠看到該變量的最新值。 4. 線程啓動規則 線程的start()方法先於它的每一個動作,即如果線程A在執行線程B 的start方法之前修改了共享變量的值,那麼當線程B執行start方法時,線程A對共享 變量的修改對線程B可見 5. 傳遞性 A先於B ,B先於C 那麼A必然先於C 6. 線程終止規則 線程的所有操作先於線程的終結,Thread.join()方法的作用是等待 當前執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的 join方法成功返回後,線程B對共享變量的修改將對線程A可見。 7. 線程中斷規則 對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到 中斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。 8. 對象終結規則對象的構造函數執行,結束先於finalize()方法

6 volitile的內存語義

	volatile內存語義 
        volatile是Java虛擬機提供的輕量級的同步機制。volatile關鍵字有如下兩個作用 保證被volatile修飾的共享變量對所有線程總數可見的,也就是當一個線程修改 了一個被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。 禁止指令重排序優化
volatile無法保證原子性
    在併發場景下,i變量的任何改變都會立馬反應到其他線程中,但是如此存在多條線程 同時調用increase()方法的話,就會出現線程安全問題,畢竟i++;操作並不具備原子性,該 操作是先讀取值,然後寫回一個新值,相當於原來的值加上1,分兩步完成,如果第二個線 程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那麼第二個線程就會與第一個線程 一起看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗,因此對於 increase方法必須使用synchronized修飾,以便保證線程安全,需要注意的是一旦使用 synchronized修飾方法後,由於synchronized本身也具備與volatile相同的特性,即可見 性,因此在這樣種情況下就完全可以省去volatile修飾變量。
	volatile禁止重排優化 
        volatile關鍵字另一個作用就是禁止指令重排優化,從而避免多線程環境下程序出現亂 序執行的現象,關於指令重排優化前面已詳細分析過,這裏主要簡單說明一下volatile是如 何實現禁止指令重排優化的。先了解一個概念,內存屏障(Memory Barrier)。 
                                                                                             硬件層的內存屏障
                                                                                                  Intel硬件提供了一系列的內存屏障,主要有: 1. lfence,是一種Load Barrier 讀屏障 2. sfence, 是一種Store Barrier 寫屏障 3. mfence, 是一種全能型的屏障,具備ifence和sfence的能力 4. Lock前綴,Lock不是一種內存屏障,但是它能完成類似內存屏障的功能。Lock會對 CPU總線和高速緩存加鎖,可以理解爲CPU指令級的一種鎖。它後面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令

1655199588733

	內存屏障
        又稱內存柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執 行順序,二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。由於 編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴 編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插 入內存屏障禁止在內存屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用 是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。 總之,volatile變量正是通過內存屏障實現其在內存中的語義,即可見性和禁止重排優化。 下面看一個非常典型的禁止重排優化的例子DCL,如下:

1655199993296

1655199977425

1655200011434

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章