Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理分析
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 line | CPU告訴緩存中可以分配的最小存儲(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ā)兩件事情:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(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/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode或鎖信息等 |
32/64bit | Class Metadata Address | 存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針 |
32/64bit | Array 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) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖標(biāo)志位 |
---|---|---|---|---|
無(wú)鎖狀態(tài) | 對(duì)象的hashcode | 對(duì)象分代年齡 | 0 | 01 |
在運(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 Swap | CAS操作需要輸入兩個(gè)數(shù)值,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化,才交換成新值,發(fā)生了變化則不交換 |
CPU 流水線 | CPU pipeline | CPU流水線的工作方式就像工業(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)文章
Spring中DAO被循環(huán)調(diào)用的時(shí)候數(shù)據(jù)不實(shí)時(shí)更新的解決方法
這篇文章主要介紹了Spring中DAO被循環(huán)調(diào)用的時(shí)候數(shù)據(jù)不實(shí)時(shí)更新的解決方法,需要的朋友可以參考下2014-08-08SpringSecurity 默認(rèn)表單登錄頁(yè)展示流程源碼
本篇主要講解 SpringSecurity提供的默認(rèn)表單登錄頁(yè) 它是如何展示流程,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-01-01Mybatis-plus selectByMap條件查詢方式
這篇文章主要介紹了Mybatis-plus selectByMap條件查詢方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06關(guān)于kafka消費(fèi)不到遠(yuǎn)程bootstrap-server?數(shù)據(jù)的問(wèn)題
很多朋友遇到kafka消費(fèi)不到遠(yuǎn)程bootstrap-server?數(shù)據(jù)的問(wèn)題,怎么解決這個(gè)問(wèn)題,很多朋友不知所措,下面小編給大家?guī)?lái)了關(guān)于kafka消費(fèi)不到遠(yuǎn)程bootstrap-server?數(shù)據(jù)的問(wèn)題及解決方法,感興趣的朋友跟隨小編一起看看吧2021-11-11Spring的refresh()方法相關(guān)異常解析
這篇文章主要介紹了Spring的refresh()方法相關(guān)異常解析,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11如何使用pipeline和jacoco獲取自動(dòng)化測(cè)試代碼覆蓋率
這篇文章主要介紹了如何使用pipeline和jacoco獲取自動(dòng)化測(cè)試代碼覆蓋率,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11Java 發(fā)送http請(qǐng)求上傳文件功能實(shí)例
本文通過(guò)實(shí)例代碼給大家介紹了Java 發(fā)送http請(qǐng)求上傳文件功能,需要的朋友參考下吧2017-06-06Mabatis錯(cuò)誤提示Parameter index out of range的處理方法
這篇文章主要介紹了Mabatis錯(cuò)誤提示Parameter index out of range 的處理方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08