欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理分析

 更新時(shí)間:2025年01月03日 09:59:15   作者:心流時(shí)間  
本文主要介紹了Java中并發(fā)編程中常用的一些機(jī)制,包括volatile、synchronized和原子操作,volatile是輕量級(jí)的同步機(jī)制,保證了共享變量的可見性;synchronized是一種重量級(jí)的同步機(jī)制,通過(guò)加鎖和解鎖來(lái)保證線程安全

Java代碼在編譯后會(huì)變成Java字節(jié)碼,字節(jié)碼被類加載器加載到JVM里,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)換為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令。

volatile的應(yīng)用

在多線程并發(fā)編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級(jí)的synchronized,它在多處理器開發(fā)中保證了共享變量的可見性。

可見性的意思是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。

如果volatile變量修飾符使用恰當(dāng)?shù)脑?,它比synchronized的使用和執(zhí)行成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度。

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

Java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。

Java語(yǔ)言提供了volatile,在某些情況下比鎖更加方便。如果一個(gè)字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。

一些CPU術(shù)語(yǔ)的定義:

術(shù)語(yǔ)英文單詞術(shù)語(yǔ)描述
內(nèi)存屏障memory barriers是一組處理器指令,用于實(shí)現(xiàn)內(nèi)存操作的順序限制
緩沖行cache lineCPU告訴緩存中可以分配的最小存儲(chǔ)單位。處理器填寫緩存行時(shí)會(huì)加載整個(gè)緩存行,現(xiàn)代CPU需要執(zhí)行幾百次CPU指令
原子操作atomic operations不可中斷的一個(gè)或一系列操作
緩存行填充cache line fill當(dāng)處理器識(shí)別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個(gè)高速緩存行到適當(dāng)?shù)木彺妫↙1,L2,L3的或所有)
緩存命中cache hit如果進(jìn)行高速緩存填充操作的內(nèi)存位置仍然是下次處理器訪問(wèn)的地址時(shí),那么在下次時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存中讀取
寫命中writer hit當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中
寫缺失writer misses the cache一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域

java代碼中定義變量時(shí)使用volatile,JIT編譯器生成的匯編指令會(huì)多出一行,這行內(nèi)容中帶有 lock add1,

lock前綴的指令在多核處理器下會(huì)引發(fā)兩件事情:

  1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
  2. 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效(一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無(wú)效),這樣在下次訪問(wèn)相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充,處理器就能從緩存中得到最新的數(shù)據(jù)(為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作)

volatile的使用優(yōu)化

著名的Java并發(fā)編程大師Doug Lea 在JDK7的并發(fā)包里新增一個(gè)隊(duì)列集合類LinkedTransferQueue,它在使用volatile變量時(shí),用一種追加字節(jié)的方式來(lái)優(yōu)化隊(duì)列出隊(duì)和入隊(duì)的性能。

就是增加15個(gè)變量,再加上父類的value變量,一個(gè)對(duì)象的引用占用4字節(jié),共64字節(jié)。一些處理器的高速緩存行是64個(gè)字節(jié)寬,這就意味著一個(gè)隊(duì)列的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)不會(huì)在同一個(gè)緩存行。

當(dāng)然有些處理器的緩存行并非64字節(jié)寬,或者共享變量不會(huì)被頻繁地寫,java7就會(huì)智能的不使用這種追加字節(jié)的方式。(java8中我并未在LinkedTransferQueue中找到追加字節(jié)對(duì)應(yīng)的代碼,可能只在java7中有)

synchronized的實(shí)現(xiàn)原理與應(yīng)用

synchronized一直被稱為重量級(jí)鎖,但java1.6對(duì)它進(jìn)行了優(yōu)化,有些情況下它并不那么重了。

下面介紹java1.6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過(guò)程。

先來(lái)看下利用synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中的每一個(gè)對(duì)象都可以作為鎖。

具體表現(xiàn)為一下三種形式:

  • 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象
  • 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象
  • 對(duì)于同步方法塊,鎖是synchronized括號(hào)里配置的對(duì)象

當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),它首先必須得到鎖,退出或拋出異常時(shí)必須釋放鎖。那么鎖到底存在哪里呢?鎖里面會(huì)存儲(chǔ)什么信息呢?

從JVM規(guī)范中可以看到synchronized在JVM里的實(shí)現(xiàn)原理,JVM基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。

代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)的,而方法同步是使用另外一種方式實(shí)現(xiàn)的,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說(shuō)明。但是,方法的同步同樣可以使用這兩個(gè)指令來(lái)實(shí)現(xiàn)。

monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)。

任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián),并且一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。

線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),即嘗試獲得對(duì)象的鎖。

Java對(duì)象頭

synchronized用的鎖是存在Java對(duì)象頭里的。如果對(duì)象是數(shù)組類型,則虛擬機(jī)用3個(gè)字寬(Word)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類型,則用2字寬存儲(chǔ)對(duì)象頭。

在32位虛擬機(jī)中,1字寬等于4字節(jié),即32bit,Java對(duì)象頭的長(zhǎng)度如下所示:

長(zhǎng)度內(nèi)容說(shuō)明
32/64bitMark Word存儲(chǔ)對(duì)象的hashCode或鎖信息等
32/64bitClass Metadata Address存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針
32/64bitArray Length數(shù)組的長(zhǎng)度(如果當(dāng)前對(duì)象是數(shù)組)

Java對(duì)象頭里的Mark Word里默認(rèn)存儲(chǔ)對(duì)象的HashCode、分代年齡和鎖標(biāo)記位。

32位JVM的Mark Word的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下所示(Java對(duì)象頭的存儲(chǔ)結(jié)構(gòu)):

鎖狀態(tài)25bit4bit1bit是否是偏向鎖2bit鎖標(biāo)志位
無(wú)鎖狀態(tài)對(duì)象的hashcode對(duì)象分代年齡001

在運(yùn)行期間,Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。

Mark Word可能變化為存儲(chǔ)以下4種數(shù)據(jù),Mark Word的狀態(tài)變化如下所示:

在64位虛擬機(jī)下,Mark Word是64bit大小的,其存儲(chǔ)結(jié)構(gòu)如下所示:

鎖的升級(jí)與對(duì)比

Java1.6為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了"偏向鎖"和"輕量級(jí)鎖",在Java1.6中,鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。

鎖可以升級(jí)但不能降級(jí),意味著偏向鎖升級(jí)為輕量級(jí)鎖后不能再降級(jí)為偏向鎖。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

HotSpot(Java虛擬機(jī)的一種實(shí)現(xiàn))的作者經(jīng)過(guò)研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。

當(dāng)一個(gè)線程訪問(wèn)同步塊并獲得鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來(lái)加鎖和解鎖,只需要簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。

如果測(cè)試成功,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,則需要再測(cè)試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置為1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程。

(1)偏向鎖的撤銷

偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。

偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有正在執(zhí)行的字節(jié)碼)。

它會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài);如果線程仍然活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不適合作為偏向鎖,最后喚醒暫停的線圖。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程:

(2)關(guān)閉偏向鎖

編向鎖在java6和Java7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,

如有必要可以使用IVM參數(shù)來(lái)關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。

如果確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過(guò)JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。

輕量級(jí)鎖

(1)輕量級(jí)鎖加鎖

線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的 Mark Word 復(fù)制到鎖記錄中,官方稱為 Displaced Mark Word。然后線程嘗試使用CAS將對(duì)象頭中的Mark Word 替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖。

(2)輕量級(jí)鎖解鎖

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

下圖是兩個(gè)線程同時(shí)爭(zhēng)奪鎖,導(dǎo)致鎖膨脹的流程圖:

因?yàn)樽孕龝?huì)消耗CPU,為了避免無(wú)用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級(jí)成重量級(jí)鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。

當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲聯(lián)鎖時(shí),都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚配這些線程,被喚醒的線程就會(huì)進(jìn)行新一輪的奪鎖之爭(zhēng)。

鎖的優(yōu)缺點(diǎn)對(duì)比

優(yōu)點(diǎn)缺點(diǎn)使用場(chǎng)景
偏向鎖加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距如果線程間存在鎖竟?fàn)帟?huì)帶來(lái)額外的鎖撤銷的消耗適用于只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景
輕量級(jí)鎖競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度如果始終得不到鎖竟?fàn)幍木€程,使用自旋會(huì)消耗CPU追求響應(yīng)時(shí)間同步塊執(zhí)行速度非???/td>
重量級(jí)鎖線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU線程阻塞,響應(yīng)時(shí)間緩慢追求吞吐量同步塊執(zhí)行速度較慢

原子操作的實(shí)現(xiàn)原理

原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子”,而原子操作(atomic operation)意為“不可被中斷的一個(gè)或一系列操作”。

在多處理器上實(shí)現(xiàn)原子操作就變得有點(diǎn)復(fù)雜。讓我們一起來(lái)聊一聊在Intel處理器和Java里是如何實(shí)現(xiàn)原子操作的。

術(shù)語(yǔ)定義

在了解原子操作的實(shí)現(xiàn)原理前,先要了解一下相關(guān)的術(shù)語(yǔ),如下所示:

術(shù)語(yǔ)名稱英文解 釋
緩存行Cache line緩存的最小操作單位
比較并交換Compare and SwapCAS操作需要輸入兩個(gè)數(shù)值,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化,才交換成新值,發(fā)生了變化則不交換
CPU 流水線CPU pipelineCPU流水線的工作方式就像工業(yè)生產(chǎn)上的裝配流水線,在CP中由5-6個(gè)不同功能的電路單元組成一條指令處理流水線,然后將一條X86指令分成5~6步后再由這些電路單元分別執(zhí)行:這樣就能實(shí)現(xiàn)在一個(gè)CPU時(shí)鐘周期完成一條指令,因此提高CP的運(yùn)算速度
內(nèi)存順序沖突Memory order violation內(nèi)存順序沖突一般是由假共享引起的,假共享是指多個(gè)CPU同時(shí)修改同一個(gè)緩存行的不同部分而引起其中一個(gè)CPU的操作無(wú)效,當(dāng)出現(xiàn)這個(gè)內(nèi)存順序沖突時(shí),CPU必須清空流水線

處理器如何實(shí)現(xiàn)原子操作

32位IA-32處理器使用基于對(duì)緩存加鎖或總線加鎖的方式來(lái)實(shí)現(xiàn)多處理器之間的原子操作。首先處理器會(huì)自動(dòng)保證基本的內(nèi)存操作的原子性。

處理器保證從系統(tǒng)內(nèi)存中讀取或者寫入一個(gè)字節(jié)是原子的,意思是當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí),其他處理器不能訪問(wèn)這個(gè)字節(jié)的內(nèi)存地址。

Pentium 6和最新的處理器能自動(dòng)保證單處理器對(duì)同一個(gè)緩存行里進(jìn)行16/32/64位的操作是原子的,但是復(fù)雜的內(nèi)存操作處理器是不能自動(dòng)保證其原子性的,比如跨總線寬度、跨多個(gè)緩存行和跨頁(yè)表的訪問(wèn)。但是,處理器提供總線鎖定和緩存鎖定兩個(gè)機(jī)制來(lái)保證復(fù)雜內(nèi)存操作的原子性。

(1)使用總線鎖保證原子性

第一個(gè)機(jī)制是通過(guò)總線鎖保證原子性。如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫操作(i++就是經(jīng)典的讀改寫操作),那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作,這樣讀改寫操作就不是原子的,作完之后共享變量的值會(huì)和期望的不一致。

舉個(gè)例子,如果i=1,我們進(jìn)行兩次i+操作,我們期望的結(jié)果是3,但是有可能結(jié)果是2,如下圖所示:

原因可能是多個(gè)處理器同時(shí)從各自的緩存中讀取變量i,分別進(jìn)行加1操作,然后分別寫入系統(tǒng)內(nèi)存中。那么,想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時(shí)候,CPU2不能操作緩存了該共享變量?jī)?nèi)存地址的緩存。

處理器使用總線鎖就是來(lái)解決這個(gè)問(wèn)題的。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)占共享內(nèi)存。

(2)使用緩存鎖保證原子性

第二個(gè)機(jī)制是通過(guò)緩存鎖定來(lái)保證原子性。在同一時(shí)刻,我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,目前處理器在某些場(chǎng)合下使用緩存鎖定代替總線鎖定來(lái)進(jìn)行優(yōu)化。

頻繁使用的內(nèi)存會(huì)緩存在處理器的L1、L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行,并不需要聲明總線鎖,在Pentium6和目前的處理器中可以使用“緩存鎖定”的方式來(lái)實(shí)現(xiàn)復(fù)雜的原子性。所謂“緩存鎖定”是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當(dāng)它執(zhí)行鎖操作回寫到內(nèi)存時(shí),處理器不在總線上聲言LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機(jī)制來(lái)保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí),會(huì)使緩存行無(wú)效,在如圖2-3所示的例子中,當(dāng)CPU1修改緩存行中的i時(shí)使用了緩存鎖定,那么CPU2就不能同時(shí)緩存i的緩存行。 

但是有兩種情況下處理器不會(huì)使用緩存鎖定:

  • 當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個(gè)緩存行(cache line)時(shí),則處理器會(huì)調(diào)用總線鎖定。
  • 有些處理器不支持緩存鎖定。對(duì)于Intel 486和Pentium 處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定。
  • 針對(duì)以上兩個(gè)機(jī)制、我們通過(guò)Intel處理器提供了很多Lock前綴的指今來(lái)實(shí)現(xiàn)。
  • 例如,位測(cè)試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些操作數(shù)和邏輯指令(如ADD、OR)等,被這些指令操作的內(nèi)存區(qū)域就會(huì)加鎖,導(dǎo)致其他處理器不能同時(shí)訪問(wèn)它。

Java 如何實(shí)現(xiàn)原子操作

在Java中可以通過(guò)鎖和循環(huán)CAS的方式來(lái)實(shí)現(xiàn)原子操作。

使用循環(huán)CAS實(shí)現(xiàn)原子操作

IVM 中的CAS操作正是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的。

自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止,以下代碼實(shí)現(xiàn)了一個(gè)基于CAS線程安全的計(jì)數(shù)器方法safeCount和一個(gè)非線程安全的計(jì)數(shù)器count。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    private int i = 0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<Thread>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }

        for (Thread t : ts) {
            t.start();
        }
        
        // 等待所有線程執(zhí)行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicInteger.get());
        System.out.println(System.currentTimeMillis() - start);
    }

    /**
     * 使用CAS實(shí)現(xiàn)線程安全計(jì)數(shù)器
     */
    private void safeCount() {
        for (; ; ) {
            int i = atomicInteger.get();
            boolean suc = atomicInteger.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }

    /**
     * 非線程安全計(jì)數(shù)器
     */
    private void count() {
        i++;
    }

}

打印:

767343

1000000

125

從 Java1.5 開始,JDK的并發(fā)包里提供了一些類來(lái)支持原子操作,如AtomicBoolean(用原子方式更新的 boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。

這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當(dāng)前值自增1和自減1。

CAS實(shí)現(xiàn)原子操作的三大問(wèn)題

在Java并發(fā)包中有一些并發(fā)框架也使用了自旋CAS的方式來(lái)實(shí)現(xiàn)原子操作,比如LinkedTransferQueue 類的 Xfer 方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問(wèn)題:ABA問(wèn)題、循環(huán)時(shí)間長(zhǎng)開銷大、只能保證一個(gè)共享變量的原子操作。

  • ABA問(wèn)題。
  • 因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來(lái)是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。
  • ABA問(wèn)題的解決思路就是使用版本號(hào)。在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A。從Java1.5開始,JDK的Atomic包里提供了一個(gè)類AtomicStampedReference來(lái)解決 ABA問(wèn)題。
  • 這個(gè)類的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference, // 預(yù)期引用
                                 V   newReference, // 更新后的引用
                                 int expectedStamp, // 預(yù)期標(biāo)志
                                 int newStamp) { // 更新后的標(biāo)志
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
  • 循環(huán)時(shí)間長(zhǎng)開銷大。
  • 自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令,那么效率會(huì)有一定的提升。
  • pause指令有兩個(gè)作用:第一,它可以延遲流水線執(zhí)行指令(de-pipeline),使 CPU不會(huì)消耗過(guò)多的執(zhí)行資源延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零;第二,它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(MemoryOrder Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率。
  • 只能保證一個(gè)共享變量的原子操作。
  • 當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作。比如,有兩個(gè)共享變量i=2,j=a,合并一下ij=2a,然后用CAS來(lái)操作ij。從Java1.5開始,JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。

使用鎖機(jī)制實(shí)現(xiàn)原子操作

鎖機(jī)制保證了只有獲得鎖的線程才能夠操作鎖定的內(nèi)存區(qū)域。JVM內(nèi)部實(shí)現(xiàn)了很多種鎖機(jī)制,有偏向鎖、輕量級(jí)鎖和互斥鎖。

有意思的是除了偏向鎖,JVM實(shí)現(xiàn)鎖的方式都用了循環(huán)CAS,即當(dāng)一個(gè)線程想進(jìn)人同步塊的時(shí)候使用循環(huán)CAS的方式來(lái)獲取鎖,當(dāng)它退出同步塊的時(shí)候使用循環(huán)CAS釋放鎖。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論