国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

Java并發機制的底層實現原理(二)

2019-11-11 06:45:01
字體:
來源:轉載
供稿:網友

java代碼在編譯后會變成Java字節碼,字節碼被類加載器加載到JVM里,JVM執行字節碼,最終需要轉化為匯編指令在CPU上執行。Java中所使用的并發機制依賴于JVM的實現和CPU的指令。

2.1 volatile的應用

1.volatile的定義與實現

Java編程允許線程訪問共享變量,為了確保共享變量能被準備和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。如果一個字段被聲明為volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。 這里寫圖片描述

為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內存。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量鎖在緩存行的數據寫回到系統內存。在多處理器下,為了保證各個處理器緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作時,會重新從系統內存中把數據讀到處理器緩存里。

volatile的兩條實現原則: 1)Lock前綴指令會引起處理器緩存回寫到內存。 Lock前綴指令導致在執行指令期間,聲明處理器的LOCK#信號。在多處理環境中(多核),LOCK#信號確保在聲言該信號期間,處理器可以獨占任何共享內存。在最近的處理器里,LOCK#信號一般不鎖總線,而是鎖緩存。如果訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖住這塊內存區域的緩存并回寫到內存,并使用緩存一致性機制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。

2)一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。

2.2 synchronized的實現原理與應用

Java中的每一個對象都可以作為鎖。具體表現為以下3中形式:

- 對于普通同步方法,鎖時當前實例對象; - 對于靜態同步方法,鎖時當前類的Class對象; - 對于同步方法塊,鎖時synchronized括號里配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。

從JVM規范中可以看到synchronized在JVM里的實現原理,JVM基于進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是另外一種方法。但是,方法的同步同樣可以實用這兩個指令來實現。

monitorenter指令時在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處于鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,機嘗試獲得對象鎖。

2.2.1 Java對象頭

synchronized用的鎖存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象時非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等于4字節,即32bit,如圖:

這里寫圖片描述

Java對象頭里的Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如表:

這里寫圖片描述

在運行期間,Mark Word里存儲的數據會隨著鎖標志位的變化而變化。

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如表:

這里寫圖片描述

2.2.2 鎖的升級與對比

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

1.偏向鎖

HotSpot作者經過研究,大多是情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價耕地而引入了偏向鎖。當一個線程訪問同步塊并獲取的鎖時,會在對象頭和棧幀中的鎖記錄里存儲偏向線程ID,以后該線程在進入和退出同步塊是不需要進行CAS操作來加鎖和解鎖,只要簡單測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,便是線程已經獲得鎖。如果失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(標識當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象的偏向鎖指向當前線程。

(1)偏向鎖的撤鎖 偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。

下圖線程1演示了偏向鎖的初始化流程,線程2演示了偏向鎖撤銷的流程。 這里寫圖片描述

(2)關閉偏向鎖 偏向鎖在Java6和7里是默認啟用的,但是它在應用程序啟動后幾秒鐘才激活,日過有必要使用JVM參數來關閉延遲: -XX:BiasedLockingStartupDelay=0.如果確定應用程序里所有鎖通常情況下處于競爭狀態,而已關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。

2.輕量級鎖

(1)輕量級鎖加鎖 線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

(2)輕量級鎖解鎖 輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

下圖是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。 這里寫圖片描述

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

3.鎖的優缺點

這里寫圖片描述

2.3 原子操作的實現原理

原子(atomic)是“不能被進一步分割的最小粒子”。而原子操作(atomic Operation)為“不可中斷的一個或一系列操作”。

1.術語定義

這里寫圖片描述

2.處理器如何實現原子操作

處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。

(1)使用總線鎖保證原子性 第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫操作(i++就是經典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會和期望的不一致。

處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內存。

(2)使用緩存鎖住保證原子性 第二個機制是通過緩存鎖定來保證原子性。在同一時刻,我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

頻繁使用的內存會緩存在處理器的L1、L2和L3高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,并不需要聲明總線鎖,在Pentium 6和目前的處理器中可以使用“緩存鎖定”的方式來實現復雜的原子性。所謂“緩存鎖定”是指內存區域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執行鎖操作回寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。舉例當CPU1修改緩存行中的i時使用了緩存鎖定,那么CPU2就不能同時緩存i的緩存行。

但有兩種情況下處理器不會使用緩存鎖定。

第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line)時,則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對于Intel 486和Pentium處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

針對以上兩個機制,我們通過Intel處理器提供了很多Lock前綴的指令來實現。例如,位測試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些操作數和邏輯指令(如ADD、OR)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。

3.Java如何實現原子操作

在Java中可以通過鎖和循環CAS的方式來實現原子操作。

樂觀鎖用到的機制就是CAS,Compare and Swap。

CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。

(1)使用循環CAS實現原子操作 JVM中的CAS操作正式利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操作直到成功為止。

從Java 1.5開始,JDK的并發包里提供了一些類來支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1和自減1。

(2)CAS實現原子操作的三大問題 在Java并發包中有一些并發框架也使用了自旋CAS的方式來實現原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大,以及只能保證一個共享變量的原子操作。

1)ABA問題。因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A→B→A就會變成1A→2B→3A。從Java 1.5開始,JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。 2)循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。 3)只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。

(3)使用鎖機制實現原子操作 鎖機制保證了只有獲得鎖的線程才能夠操作鎖定的內存區域。JVM內部實現了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了循環CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 会同县| 绩溪县| 库尔勒市| 青州市| 泸定县| 南城县| 青浦区| 花莲市| 锦州市| 青冈县| 晋江市| 乐昌市| 那坡县| 永仁县| 大邑县| 平泉县| 棋牌| 工布江达县| 桂阳县| 玛曲县| 太康县| 东光县| 正镶白旗| 延寿县| 永靖县| 南澳县| 湄潭县| 营口市| 平和县| 淮北市| 榕江县| 平定县| 壶关县| 定边县| 白朗县| 象山县| 许昌县| 留坝县| 金昌市| 岚皋县| 岚皋县|