感謝:https://blog.csdn.net/zqz_zqz/article/details/70233767java
以前作過一個測試,詳情見這篇文章《多線程 +1操做的幾種實現方式,及效率對比》,當時對這個測試結果很疑惑,反覆執行過屢次,發現結果是同樣的:
1. 單線程下synchronized效率最高(當時感受它的效率應該是最差纔對);
2. AtomicInteger效率最不穩定,不一樣併發狀況下表現不同:短期低併發下,效率比synchronized高,有時甚至比LongAdder還高出一點,可是高併發下,性能還不如synchronized,不一樣狀況下性能表現很不穩定;
3. LongAdder性能穩定,在各類併發狀況下表現都不錯,總體表現最好,短期的低併發下比AtomicInteger性能差一點,長時間高併發下性能最高(可讓AtomicInteger下臺了);數組
這篇文章咱們就去揭祕,爲何會是這個測試結果!緩存
若是想要透徹的理解java鎖的前因後果,須要先了解如下基礎知識。安全
鎖從宏觀上分類,分爲悲觀鎖與樂觀鎖。數據結構
樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。多線程
java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。併發
悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。框架
java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。jvm
synchronized會致使爭用不到鎖的線程進入阻塞狀態,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖。函數
明確java線程切換的代價,是理解java中各類鎖的優缺點的基礎之一。
在介紹java鎖以前,先說下什麼是markword,markword是java對象數據結構中的一部分,要詳細瞭解java對象的結構能夠點擊這裏,這裏只作markword的詳細介紹,由於對象的markword和java各類類型的鎖密切相關;
markword數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:
狀態 | 標誌位 | 存儲內容 |
---|---|---|
未鎖定 | 01 | 對象哈希碼、對象分代年齡 |
輕量級鎖定 | 00 | 指向鎖記錄的指針 |
膨脹(重量級鎖定) | 10 | 執行重量級鎖定的指針 |
GC標記 | 11 | 空(不須要記錄信息) |
可偏向 | 01 | 偏向線程ID、偏向時間戳、對象分代年齡 |
32位虛擬機在不一樣狀態下markword結構以下圖所示:
瞭解了markword結構,有助於後面瞭解java鎖的加鎖解鎖過程;
前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖,
不一樣的鎖有不一樣特色,每種鎖只有在其特定的場景下,纔會有出色的表現,java中沒有哪一種鎖可以在全部狀況下都能有出色的效率,引入這麼多鎖的緣由就是爲了應對不一樣的狀況;
前面講到了重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬於樂觀鎖,因此如今你就可以大體理解了他們的適用範圍,可是具體如何使用這幾種鎖呢,就要看後面的具體分析他們的特性;
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
可是線程自旋是須要消耗cup的,說白了就是讓cup在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用cup自旋作無用功,因此須要設定一個自旋等待的最大時間。
若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。
自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換!
可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用cpu作無用功,佔着XX不XX,同時有大量線程在競爭一個鎖,會致使獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要cup的線程又不能獲取到cpu,形成cpu的浪費。因此這種狀況下咱們要關閉自旋鎖;
自旋鎖的目的是爲了佔着CPU的資源不釋放,等到獲取到鎖當即進行處理。可是如何去選擇自旋的執行時間呢?若是自旋執行時間太長,會有大量的線程處於自旋狀態佔用CPU資源,進而會影響總體系統的性能。所以自旋的週期選的額外重要!
JVM對於自旋週期的選擇,jdk1.5這個限度是必定的寫死的,在1.6引入了適應性自旋鎖,適應性自旋鎖意味着自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間,同時JVM還針對當前CPU的負荷狀況作了較多的優化
若是平均負載小於CPUs則一直自旋
若是有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞
若是正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞
若是CPU處於節電模式則中止自旋
自旋時間的最壞狀況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差)
自旋時會適當放棄線程優先級之間的差別
JDK1.6中-XX:+UseSpinning開啓;
-XX:PreBlockSpin=10 爲自旋次數;
JDK1.7後,去掉此參數,由jvm控制;
在JDK1.5以前都是使用synchronized關鍵字保證同步的,Synchronized的做用相信你們都已經很是熟悉了;
它能夠把任意一個非NULL的對象看成鎖。
實現以下圖所示;
它有多個隊列,當多個線程一塊兒訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不一樣的容器中。
Contention List:競爭隊列,全部請求鎖的線程首先被放在這個競爭隊列中;
Entry List:Contention List中那些有資格成爲候選資源的線程被移動到Entry List中;
Wait Set:哪些調用wait方法被阻塞的線程被放置在這裏;
OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲OnDeck;
Owner:當前已經獲取到所資源的線程被稱爲Owner;
!Owner:當前釋放鎖的線程。
JVM每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),可是併發狀況下,ContentionList會被大量的併發線程進行CAS訪問,爲了下降對尾部元素的競爭,JVM會將一部分線程移動到EntryList中做爲候選競爭線程。Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程爲OnDeck線程(通常是最早進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交給OnDeck,OnDeck須要從新競爭鎖。這樣雖然犧牲了一些公平性,可是能極大的提高系統的吞吐量,在JVM中,也把這種選擇行爲稱之爲「競爭切換」。
OnDeck線程獲取到鎖資源後會變爲Owner線程,而沒有獲得鎖資源的仍然停留在EntryList中。若是Owner線程被wait方法阻塞,則轉移到WaitSet隊列中,直到某個時刻經過notify或者notifyAll喚醒,會從新進去EntryList中。
處於ContentionList、EntryList、WaitSet中的線程都處於阻塞狀態,該阻塞是由操做系統來完成的(Linux內核下采用pthread_mutex_lock內核函數實現的)。
Synchronized是非公平鎖。 Synchronized在線程進入ContentionList時,等待的線程會先嚐試自旋獲取鎖,若是獲取不到就進入ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶佔OnDeck線程的鎖資源。
Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。
偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,這種狀況下,就會給線程加一個偏向鎖。
若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
它經過消除資源無競爭狀況下的同步原語,進一步提升了程序的運行性能。
偏向鎖獲取過程:
訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01,確認爲可偏向狀態。
若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟5,不然進入步驟3。
若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行5;若是競爭失敗,執行4。
若是CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會致使stop the word)
執行同步代碼。
注意:第四步中到達安全點safepoint會致使stop the word,時間很短。
偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。
始終只有一個線程在執行同步塊,在它沒有執行完釋放鎖以前,沒有其它線程去執行同步塊,在鎖無競爭的狀況下使用,一旦有了競爭就升級爲輕量級鎖,升級爲輕量級鎖的時候須要撤銷偏向鎖,撤銷偏向鎖的時候會致使stop the word操做;
在有鎖的競爭時,偏向鎖會多作不少額外操做,尤爲是撤銷偏向所的時候會致使進入安全點,安全點會致使stw,致使性能降低,這種狀況下應當禁用;
查看停頓–安全點停頓日誌
要查看安全點停頓,能夠打開安全點日誌,經過設置JVM參數 -XX:+PrintGCApplicationStoppedTime 會打出系統中止的時間,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 這兩個參數會打印出詳細信息,能夠查看到使用偏向鎖致使的停頓,時間很是短暫,可是爭用嚴重的狀況下,停頓次數也會很是多;
注意:安全點日誌不能一直打開:
1. 安全點日誌默認輸出到stdout,一是stdout日誌的整潔性,二是stdout所重定向的文件若是不在/dev/shm,可能被鎖。
2. 對於一些很短的停頓,好比取消偏向鎖,打印的消耗比停頓自己還大。
3. 安全點日誌是在安全點內打印的,自己加大了安全點的停頓時間。
因此安全日誌應該只在問題排查時打開。
若是在生產系統上要打開,再再增長下面四個參數:
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
打開Diagnostic(只是開放了更多的flag可選,不會主動激活某個flag),關掉輸出VM日誌到stdout,輸出到獨立文件,/dev/shm目錄(內存文件系統)。
此日誌分三部分:
第一部分是時間戳,VM Operation的類型
第二部分是線程概況,被中括號括起來
total: 安全點裏的總線程數
initially_running: 安全點開始時正在運行狀態的線程數
wait_to_block: 在VM Operation開始前須要等待其暫停的線程數
第三部分是到達安全點時的各個階段以及執行操做所花的時間,其中最重要的是vmop
可見,那些不少但又很短的安全點,全都是RevokeBias, 高併發的應用會禁用掉偏向鎖。
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖;
輕量級鎖的加鎖過程:
在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。這時候線程堆棧與對象頭的狀態如圖:
所示。
拷貝對象頭中的Mark Word複製到鎖記錄中;
拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟4,不然執行步驟5。
若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示。
若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了避免讓線程阻塞,而採用循環去獲取鎖的過程。
釋放鎖線程視角:由輕量鎖切換到重量鎖,是發生在輕量鎖釋放鎖的期間,以前在獲取鎖的時候它拷貝了鎖對象頭的markword,在釋放鎖的時候若是它發如今它持有鎖的期間有其餘線程來嘗試獲取鎖了,而且該線程對markword作了修改,二者比對發現不一致,則切換到重量鎖。
由於重量級鎖被修改了,全部display mark word和原來的markword不同了。
怎麼補救,就是進入mutex前,compare一下obj的markword狀態。確認該markword是否被其餘線程持有。
此時若是線程已經釋放了markword,那麼經過CAS後就能夠直接進入線程,無需進入mutex,就這個做用。
嘗試獲取鎖線程視角:若是線程嘗試獲取鎖的時候,輕量鎖正被其餘線程佔有,那麼它就會修改markword,修改重量級鎖,表示該進入重量鎖了。
還有一個注意點:等待輕量鎖的線程不會阻塞,它會一直自旋等待鎖,並如上所說修改markword。
這就是自旋鎖,嘗試獲取鎖的線程,在沒有得到鎖的時候,不被掛起,而轉而去執行一個空循環,即自旋。在若干個自旋後,若是尚未得到鎖,則才被掛起,得到鎖,則執行代碼。
synchronized的執行過程:
1. 檢測Mark Word裏面是否是當前線程的ID,若是是,表示當前線程處於偏向鎖
2. 若是不是,則使用CAS將當前線程的ID替換Mard Word,若是成功則表示當前線程得到偏向鎖,置偏向標誌位1
3. 若是失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
4. 當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,若是成功,當前線程得到鎖
5. 若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
6. 若是自旋成功則依然處於輕量級狀態。
7. 若是自旋失敗,則升級爲重量級鎖。
上面幾種鎖都是JVM本身內部實現,當咱們執行synchronized同步塊的時候jvm會根據啓用的鎖和當前線程的爭用狀況,決定如何執行同步操做;
在全部的鎖都啓用的狀況下線程進入臨界區時會先去獲取偏向鎖,若是已經存在偏向鎖了,則會嘗試獲取輕量級鎖,啓用自旋鎖,若是自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的線程阻塞掛起,直到持有鎖的線程執行完同步塊喚醒他們;
偏向鎖是在無鎖爭用的狀況下使用的,也就是同步開在當前線程沒有執行完以前,沒有其它線程會執行該同步塊,一旦有了第二個線程的爭用,偏向鎖就會升級爲輕量級鎖,若是輕量級鎖自旋到達閾值後,沒有獲取到鎖,就會升級爲重量級鎖;
若是線程爭用激烈,那麼應該禁用偏向鎖。
以上介紹的鎖不是咱們代碼中可以控制的,可是借鑑上面的思想,咱們能夠優化咱們本身線程的加鎖操做;
不須要同步執行的代碼,能不放在同步快裏面執行就不要放在同步快內,可讓鎖儘快釋放;
它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增長並行度,從而下降鎖競爭。它的思想也是用空間來換時間;
java中不少數據結構都是採用這種方法提升併發操做的效率:
java中的ConcurrentHashMap在jdk1.8以前的版本,使用一個Segment 數組
Segment< K,V >[] segments
Segment繼承自ReenTrantLock,因此每一個Segment就是個可重入鎖,每一個Segment 有一個HashEntry< K,V >數組用來存放數據,put操做時,先肯定往哪一個Segment放數據,只須要鎖定這個Segment,執行put,其它的Segment不會被鎖定;因此數組中有多少個Segment就容許同一時刻多少個線程存放數據,這樣增長了併發能力。
LongAdder 實現思路也相似ConcurrentHashMap,LongAdder有一個根據當前併發情況動態改變的Cell數組,Cell對象裏面有一個long類型的value用來存儲值;
開始沒有併發爭用的時候或者是cells數組正在初始化的時候,會使用cas來將值累加到成員變量的base上,在併發爭用的狀況下,LongAdder會初始化cells數組,在Cell數組中選定一個Cell加鎖,數組有多少個cell,就容許同時有多少線程進行修改,最後將數組中每一個Cell中的value相加,在加上base的值,就是最終的值;cell數組還能根據當前線程爭用狀況進行擴容,初始長度爲2,每次擴容會增加一倍,直到擴容到大於等於cpu數量就再也不擴容,這也就是爲何LongAdder比cas和AtomicInteger效率要高的緣由,後面二者都是volatile+cas實現的,他們的競爭維度是1,LongAdder的競爭維度爲「Cell個數+1」爲何要+1?由於它還有一個base,若是競爭不到鎖還會嘗試將數值加到base上;
LinkedBlockingQueue也體現了這樣的思想,在隊列頭入隊,在隊列尾出隊,入隊和出隊使用不一樣的鎖,相對於LinkedBlockingArray只有一個鎖效率要高;
拆鎖的粒度不能無限拆,最多能夠將一個鎖拆爲當前cup數量個鎖便可;
大部分狀況下咱們是要讓鎖的粒度最小化,鎖的粗化則是要增大鎖的粒度;
在如下場景下須要粗化鎖的粒度:
假若有一個循環,循環內的操做須要加鎖,咱們應該把鎖放到循環外面,不然每次進出循環,都進出一次臨界區,效率是很是差的;
ReentrantReadWriteLock 是一個讀寫鎖,讀操做加讀鎖,能夠併發讀,寫操做使用寫鎖,只能單線程寫;
CopyOnWriteArrayList 、CopyOnWriteArraySet
CopyOnWrite容器即寫時複製的容器。通俗的理解是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器。
CopyOnWrite併發容器用於讀多寫少的併發場景,由於,讀的時候沒有鎖,可是對其進行更改的時候是會加鎖的,不然會致使多個線程同時複製出多個副本,各自修改各自的;
若是須要同步的操做執行速度很是快,而且線程競爭並不激烈,這時候使用cas效率會更高,由於加鎖會致使線程的上下文切換,若是上下文切換的耗時比同步操做自己更耗時,且線程對資源的競爭不激烈,使用volatiled+cas操做會是很是高效的選擇;
除了咱們在代碼中使用的同步鎖和jvm本身內置的同步鎖外,還有一種隱藏的鎖就是緩存行,它也被稱爲性能殺手。
在多核cup的處理器中,每一個cup都有本身獨佔的一級緩存、二級緩存,甚至還有一個共享的三級緩存,爲了提升性能,cpu讀寫數據是以緩存行爲最小單元讀寫的;32位的cpu緩存行爲32字節,64位cup的緩存行爲64字節,這就致使了一些問題。
例如,多個不須要同步的變量由於存儲在連續的32字節或64字節裏面,當須要其中的一個變量時,就將它們做爲一個緩存行一塊兒加載到某個cup-1私有的緩存中(雖然只須要一個變量,可是cpu讀取會以緩存行爲最小單位,將其相鄰的變量一塊兒讀入),被讀入cpu緩存的變量至關因而對主內存變量的一個拷貝,也至關於變相的將在同一個緩存行中的幾個變量加了一把鎖,這個緩存行中任何一個變量發生了變化,當cup-2須要讀取這個緩存行時,就須要先將cup-1中被改變了的整個緩存行更新回主存(即便其它變量沒有更改),而後cup-2纔可以讀取,而cup-2可能須要更改這個緩存行的變量與cpu-1已經更改的緩存行中的變量是不同的,因此這至關於給幾個絕不相關的變量加了一把同步鎖;
爲了防止僞共享,不一樣jdk版本實現方式是不同的:
1. 在jdk1.7以前會 將須要獨佔緩存行的變量先後添加一組long類型的變量,依靠這些無心義的數組的填充作到一個變量本身獨佔一個緩存行;
2. 在jdk1.7由於jvm會將這些沒有用到的變量優化掉,因此採用繼承一個聲明瞭好多long變量的類的方式來實現;
3. 在jdk1.8中經過添加sun.misc.Contended註解來解決這個問題,若要使該註解有效必須在jvm中添加如下參數:
-XX:-RestrictContended
sun.misc.Contended註解會在變量前面添加128字節的padding將當前變量與其餘變量進行隔離;
關於什麼是緩存行,jdk是如何避免緩存行的,網上有很是多的解釋,在這裏就再也不深刻講解了;
其它方式等待着你們一塊兒補充