原子性、可见性、有序性解决方案
原子性、可見性、有序性解決方案
(一)原子性
原子性是指:一個或多個操作,要么全部執行且在執行過程中不被任何因素打斷,要么全部不執行。在Java中當我們討論一個操作具有原子性問題是一般就是指這個操作會被線程的隨機調度打斷。
JMM對原子性的保證大概分以下幾種類型:java自帶原子性、synchronized、Lock鎖、原子操作類(CAS)。下面我們來一個一個細說。
1. java自帶原子性
在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,但是long和double類型是64位,在32位JVM中會將64位數據的讀寫操作分成兩次32位來處理,所以long和double在32位JVM中是非原子操作,也就是說在并發訪問時是非線程安全的。
尤其要注意,這里我們講的僅僅是讀取和賦值兩種操作具有原子性。這里的賦值僅僅指具體數值的賦值,而不包括變量給變量賦值。另外,組合操作(例如++和–操作)也同樣不具有原子性。我們可以看幾個經典例子:
a = true; // 原子性 a = 5; // 原子性 a = b; // 非原子性,分兩步完成,第一步加載b的值,第二步將b賦值給a a = b + 2; // 非原子性,分三步完成 a++; // 非原子性,分三步完成了解匯編語言的朋友很容易理解下面三個例子為什么不能一步完成。不了解匯編語言的朋友記住即可,這個地方非常關鍵。
2. synchronized
synchronized可以保證操作結果的原子性(注意這里的描述)。synchronized保證原子性的原理也很簡單,因為synchronized可以防止多個線程并發執行同一段代碼。
方法加了synchronized后,當一個線程沒執行完這個方法前,其他線程是不能執行這段代碼的。其實我們發現synchronized并不能將代碼變成原子性操作,代碼在執行過程中還是有可能被中斷的,但是,即使被中斷了其他線程也不能乘機突然進入臨界區執行這段代碼,當之前被中斷的線程繼續執行直到結束時得到的結果還是正確的。
因此,synchronized對原子性問題的保證是從最終結果上來保證的,也就是說它只保證最終的結果正確,中間操作的是否被打斷沒法保證。
3. Lock鎖
Lock鎖的原理與synchronized基本一致,因此不再贅述。
4. 原子操作類(CAS)
JDK提供了很多原子操作類來保證操作的原子性。原子操作類的底層是使用CAS機制的,這個機制對原子性的保證和synchronized有本質的區別。CAS機制保證了整個賦值操作是原子的不能被打斷的,而synchronized只能保證代碼最后執行結果的正確性,也就是說synchronized能消除原子性問題對代碼最后執行結果的影響,但原子操作類(CAS)是真正保證了操作的原子性。
(二)可見性
Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程在工作內存中保存的值是主內存中值的副本,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存,等到線程對變量操作完畢之后會將變量的最新值刷新回到主內存。
但是何時刷新這個最新值又是隨機的。所以就有可能一個線程已經將一個共享變量更新了,但是還沒刷新回主內存,那么這時其他對這個變量進行讀寫的線程就看不到這個最新值。還有一種可能就是雖然修改線程已經將最新值刷新到主內存中去了,但是讀線程的工作內存中副本的緩存值還沒過期,那么讀線程還是會使用這個副本值,而不是主內存中的最新值。這個就是多CPU多線程編程環境下的可見性問題。
JMM針對可見性問題提出了下面幾種解決方案:volatile、synchronized、Lock鎖、原子操作類(CAS),下面一個個細說。
1. volatile
我們可以看一下volatile究竟做了什么。使用volatile修飾一個共享變量可以達到如下的效果(內存語義):
volatile底層使用的是內存屏障來保證可見性的。我們先來了解一下內存屏障。
內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作。大多數現代計算機為了提高性能而采取亂序執行,這使得內存屏障成為必須。語義上,內存屏障之前的所有寫操作都要寫入內存;內存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結果。因此,對于敏感的程序塊,在寫操作之后、讀操作之前可以插入內存屏障。
我們可以從上面一大段定義中抽出兩條要點來對應volatile的內存語義:內存屏障之前的寫操作都必須立馬刷新回主內存、內存屏障之后的讀操作都必須從主內存中讀取最新值。這也就是volatile保證可見性的基本原理。
2. synchronized
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。這樣就保證了可見性。
從這里我們發現,實際上鎖具有和volatile一致的內存語義,所以使用synchronized也可以實現共享變量的可見性。
3. Lock鎖
Lock鎖的原理與synchronized基本一致,因此不再贅述。
4. 原子操作類(CAS)
使用原子操作類也可以保證共享變量操作的可見性。原子操作類底層使用的是CAS機制。Java中CAS機制每次都會從主內存中獲取最新值進行compare,比較一致之后才會將新值set到主內存中去。而且這個整個操作是一個原子操作。所以CAS操作每次拿到的都是主內存中的最新值,每次set的值也會立即寫到主內存中。
(三)有序性
為什么會出現有序性問題,其根源就是指令重排。指令重排是指編譯器和處理器在不影響代碼單線程執行結果的前提下,對源代碼的指令進行重新排序執行。這種重排序執行是一種優化手段,目的是為了處理器內部的運算單元能盡量被充分利用,提升程序的整體運行效率。
重排序分為以下幾種:
處理器為了提升程序的性能,可以對程序進行重排序。但是必須滿足重排序之后的代碼在單線程環境下執行的結果不能改變(很關鍵),這個原則也就是我們常說的as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。
但是,as-if-serial只能保證單線程情況下結果保持不變,在多線程情況下就無法保證。因此,多線程下的有序性問題可能會導致最終的結果發生無法預測的變化,這是一個非常嚴重的問題。
因此,JMM使用了四種方式來確保有序性:happens-before原則、synchronized、Lock鎖、volatile,下面我們一一細說:
1. happens-before原則
我們先來看一下《java并發編程的藝術》中的定義:
在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens- before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the f irst is visible toand ordered before the second)
我們尤其要注意:如果a happen-before b,則a所做的任何操作對b是可見的。這一點大家務必要記住,因為happen-before這個詞很容易被誤解為是時間的前后。
下面是Java內存模型一些自帶的先行發生關系(摘自《java并發編程的藝術》)這些先行發生關系無須任何同步器協助就已經存在,可以在編碼中直接使用。 如果兩個操作之間的關系不在此列,并且無法從下列規則間接推導出來的話,它們就沒有有序性保障,虛擬機可以對它們隨意地進行重排序:
如果不能滿足happens-before原則,就需要使用synchronized機制或volatile機制機制來保證有序性。
2. synchronized
synchronized保證有序性的方法非常簡單粗暴,但是這也就帶來了更大的資源浪費。synchronized語義表示鎖在同一時刻只能由一個線程進行獲取,當鎖被占用后,其他線程只能等待。因此,synchronized語義就要求線程在訪問讀寫共享變量時只能“串行”執行,因此synchronized具有有序性。
3. Lock鎖
Lock鎖的原理與synchronized基本一致,因此不再贅述。
4. volatile
volatile的底層是使用內存屏障來保證有序性的。若用volatile修飾共享變量,在JVM底層volatile是采用“內存屏障”來實現禁止特定類型的處理器重排序。加入volatile關鍵字時,會多出一個lock前綴指令,lock前綴指令實際上相當于一個內存屏障(也成內存柵欄)。內存屏障可以保證:
(四)總結
我們把上述要點總結一下,實際上主要就是三個關鍵點:volatile無法保證原子性、synchronized只能保證原子性問題不影響最終結果但無法真正保證原子性、原子類無法保證有序性。
2020年7月11日
總結
以上是生活随笔為你收集整理的原子性、可见性、有序性解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 粘贴应变片步骤及注意事项
- 下一篇: 2021-5-8字符串作业