java原子操作
1. 什麼是原子操作,java中的原子操作是什麼
"原子操作(atomic operation)是不需要synchronized",所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch;
java中一般事務管理裡面用到原子操作。
2. java原子操作會成為性能瓶頸嗎
會,原子性操作,意味著那段時間內 別人不能操作。操作就回滾,10000筆操作同時進老絕來,每個5ms,那麼意味著總共50000ms,才能跑完,或者等不了那麼久,你侍拍姿就只能賀鄭完成一部分,然後及時反饋給用戶系統繁忙。
3. 請問java中的原子操作有哪些
13是, 24不是, 但並不是樓上說的意思哦, 原子操作可以和多線程結合起來看。
首先樓主你要知道原子操作是什麼局巧困,我的理解是符合多線程原子性操作的操作就叫原子操作。
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型)這個操作
是不可分割的,那麼我們說這個操作是原子操作。再比如:a++;這個操作實際是a = a + 1;
是可分割的,所以他不是一個原子操作。
例如 :你執行a = 1這個操作的時候, 是沒有任何寬缺問題的, 但是當你執行a = b 的時候就有很大的問題了
假如這個時候別的線程改變了b的值, 那麼桐念a = b就會出現不同的結果, 因為b的值你並不能確定,
比如你第期望的是a = b,你認為b的值是3,所以a也是3,可是多線程情況下程序使b變成了
4,那麼a也就變成了4,那這就不算一個原子操作。
4. CAS 與原子操作
鎖可以從不同的角度分類。其中,樂觀鎖和悲觀鎖是一種分類方式。
樂觀鎖:
樂觀鎖又稱為「無鎖」。樂觀鎖總是假設對共享資源的訪問沒有沖突,線程可以不停地執行,無需加鎖也無需等待。而一旦多個線程發生沖突,樂觀鎖通常是使用一種稱為 CAS 的技術來保證線程執行的安全性。
由於無鎖操作中沒有鎖的存在,因此不可能出現死鎖的情況,也就是說 樂觀鎖免疫死鎖 。
樂觀鎖多用於「讀多寫少「的環境,避免頻繁加鎖影響性能;而悲觀鎖多用於」寫多讀少「的環境,避免頻繁失敗和重試影響性能。
悲觀鎖:
悲觀鎖就是我們常說的鎖。對於悲觀鎖來說,它總是認為每次訪問共享資源時會發生沖突,所以必須對每次數據操作加上鎖,以保證臨界區的程序同一時間只能有一個線程在執行。
在Java中可以通過鎖和循環 CAS 的方式來實現原子操作。
CAS 的全稱是:比較並交換(Compare And Swap)。在CAS中,有這樣三個值:
比較並交換的過程如下:
CAS 指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改為B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。
我們以一個簡單的例子來解釋這個過程:
在這個例子中,i就是V,5就是A,6就是B。
那有沒有可能我在判斷了 i 為凱轎5之後,正准備更新它的新值的時候,被其它線程更改了 i 的值呢?
不會的。因為CAS是一種原子操作,它是一種系統原語,是一條CPU的原子指令,從CPU層面保證它的原子性
當多個線程同時使用CAS操作一個變數時,只有一個會勝出,並成功更新,其餘均會失敗,但失敗的線程並不會被掛起,僅是被告知失敗,並且允許盯吵肆再次嘗試,當然也允許失敗的線程放棄操作。
CAS 的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。循環 CAS 就是在一個循環里不斷的做 cas 操作,直到成功為止。
CAS 是怎麼實現線程的安全呢?
我們將其交給硬體 — CPU 和內存,利用 CPU 的多處理能力,實現硬體層面的阻塞,再加上 volatile 變數的特性即可實現基於原子操作的線程安全。
CAS 是一種無鎖演算法,通過硬體層面上對先後操作內存的線程進行排隊處理,CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什麼都不做。
CAS(比較並交換)是CPU指令級的操作, 只有一步原子操作,碰前所以非常快 。而且避免了請求操作系統來裁定鎖的問題,不用麻煩操作系統,直接在CPU內部就搞定了
1、ABA 問題
CAS 在操作的時候會檢查變數的值是否被更改過,如果沒有則更新值,但是帶來一個問題,最開始的值是A,接著變成B,最後又變成了A。經過檢查這個值確實沒有修改過,因為最後的值還是A,但是實際上這個值確實已經被修改過了。為了解決這個問題,在每次進行操作的時候加上一個 版本號 ,每次操作的就是兩個值,一個版本號和某個值,A——>B——>A問題就變成了1A——>2B——>3A。在 jdk 中提供了 AtomicStampedReference 類解決ABA問題,用Pair這個內部類實現,包含兩個屬性,分別代表版本號和引用。
這個類的 compareAndSet 方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前版本號標志是否等於預期版本號標志,如果二者都相等,才使用CAS設置為新的值和標志。
2、循環時間長開銷大
自旋 CAS 如果長時間不成功,會佔用大量的 CPU 資源,給 CPU 帶來非常大的執行開銷。
解決思路是讓 JVM 支持處理器提供的 pause 指令 。
pause 指令 能讓自旋失敗時 cpu 睡眠一小段時間再繼續自旋,從而使得讀操作的頻率低很多,為解決內存順序沖突而導致的CPU流水線重排的代價也會小很多。
3、只能保證一個共享變數的原子操作
當對一個共享變數執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變數操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
還有一個取巧的辦法,就是把多個共享變數合並成一個共享變數來操作。比 如,有兩個共享變數 i=2,j=a,合並一下 ij=2a,然後用 CAS 來操作 ij。從 Java 1.5 開始,JDK 提供了 AtomicReference 類來保證引用對象之間的原子性,就可以把多個變數放在一個對象里來進行 CAS 操作。
解決方案:
AtomicInteger
實例:
列印結果:12。
ai.compareAndSet(10, 12); 改為 ai.compareAndSet(11, 12);時,列印結果:10
AtomicIntegerArray
主要是提供原子的方式更新數組里的整型,其常用方法如下。
需要注意的是,數組 value 通過構造方法傳遞進去,然後 AtomicIntegerArray 會將當前數組復制一份,所以當 AtomicIntegerArray 對內部的數組元素進行修改時,不會影響傳入的數組。
實例
列印結果:
原子更新基本類型的 AtomicInteger,只能更新一個變數,如果要原子更新多個變數,就需要使用這個原子更新引用類型提供的類。Atomic 包提供了以下 3 個類。
**AtomicReference **
原子更新引用類型。
實例:
列印結果:
AtomicStampedReference
利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在 ABA 問題了。這就是 AtomicStampedReference 的解決方案。AtomicMarkableReference 跟 AtomicStampedReference 差不多,AtomicStampedReference 是使用 pair 的 int stamp 作為計數器使用,AtomicMarkableReference 的 pair 使用的是 boolean mark。 AtomicStampedReference 可能關心的是動過幾次, AtomicMarkableReference 關心的是有沒有被人動過,方法都比較簡單。
實例:
列印結果:
AtomicMarkableReference
原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)。
如果需原子地更新某個類里的某個欄位時,就需要使用原子更新欄位類, Atomic 包提供了以下 3 個類進行原子欄位更新。 要想原子地更新欄位類需要兩步。第一步,因為原子更新欄位類都是抽象類, 每次使用的時候必須使用靜態方法 newUpdater() 創建一個更新器,並且需要設置想要更新的類和屬性。第二步,更新類的欄位(屬性)必須使用 public volatile 修飾符。
AtomicIntegerFieldUpdater: 原子更新整型的欄位的更新器。
AtomicLongFieldUpdater: 原子更新長整型欄位的更新器。
AtomicReferenceFieldUpdater: 原子更新引用類型里的欄位。
5. Java多線程之Atomic:原子變數與原子類
一 何謂Atomic?
Atomic一詞跟原子有點關系 後者曾被人認為是最小物質的單位 計算機中的Atomic是指不能分割成若幹部分的意思 如果一段代碼被認為是Atomic 則表示這段代碼在執行過程中 是不能被中斷的 通常來說 原子指令由硬體提供尺虧襲 供軟體來實現原子方法(某個線程進入該方法後 就不會被中斷 直到其執行完成)
在x 平台上 CPU提供了在指令執行期間對匯流排加鎖的手段 CPU晶元上有一條引線#HLOCK pin 如果匯編語言的程序中在一條指令前面加上前綴 LOCK 經過匯編以後的機器代碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低 持續到這條指令結束時放開 從而把匯流排鎖住 這樣同一匯流排上別的CPU就暫時不能通過匯流排訪問內存了 保證了這條指陵兄令在多處理器環境中的原子性
二 ncurrent中的原子變數
無論是直接的還是間接的 幾乎 ncurrent 包中的所有類都使用原子變數 而不使用同步 類似 ConcurrentLinkedQueue 的類也使用原子變數直接實現無等待演算法 而類似 ConcurrentHashMap 的類使用 ReentrantLock 在需要時進行鎖定 然後 ReentrantLock 使用原子變數來維護等待鎖定的線程隊列
如果沒有 JDK 中的 JVM 改進 將無法構造這些類 這些改進暴露了(向類庫 而不是用戶類)介面來訪問硬體級的同步原語 然後 ncurrent 中的原子變數類和其他類向用戶類公開這些功能
ncurrent atomic的原子類
這個包裡面提供了一組原子類 其基本的特性就是在多線程環境下 當有多個線程同時執行這些類的實例包含的方法時 具有排他性 即當某個線程進入方法 執行其中的指令時 不會被其他線程打斷 而別的線程就像自旋鎖一樣 一直等到該方法執行完成 才由JVM從等待隊列中選擇一個另一個線程進入 這只是一種邏輯上的理解 實際上是藉助硬體的相關指令來實現的 不會阻塞線程(或者說只是在硬體級別上阻塞了) 其中的類可以分成 組
AtomicBoolean AtomicInteger AtomicLong AtomicReference
AtomicIntegerArray AtomicLongArray
AtomicLongFieldUpdater AtomicIntegerFieldUpdater AtomicReferenceFieldUpdater
AtomicMarkableReference AtomicStampedReference AtomicReferenceArray
其中AtomicBoolean AtomicInteger AtomicLong AtomicReference是類似空好的
首先AtomicBoolean AtomicInteger AtomicLong AtomicReference內部api是類似的 舉個AtomicReference的例子
使用AtomicReference創建線程安全的堆棧
Java代碼
public class LinkedStack<T> {
private AtomicReference<Node<T》 stacks = new AtomicReference<Node<T》()
public T push(T e) {
Node<T> oldNode newNode;
while (true) { //這里的處理非常的特別 也是必須如此的
oldNode = stacks get()
newNode = new Node<T>(e oldNode)
if (pareAndSet(oldNode newNode)) {
return e;
}
}
}
public T pop() {
Node<T> oldNode newNode;
while (true) {
oldNode = stacks get()
newNode = oldNode next;
if (pareAndSet(oldNode newNode)) {
return oldNode object;
}
}
}
private static final class Node<T> {
private T object;
private Node<T> next;
private Node(T object Node<T> next) {
this object = object;
this next = next;
}
}
}
然後關注欄位的原子更新
AtomicIntegerFieldUpdater<T>/AtomicLongFieldUpdater<T>/AtomicReferenceFieldUpdater<T V>是基於反射的原子更新欄位的值
相應的API也是非常簡
單的 但是也是有一些約束的
( )欄位必須是volatile類型的!volatile到底是個什麼東西 請查看
( )欄位的描述類型(修飾符public/protected/default/private)是與調用者與操作對象欄位的關系一致 也就是說調用者能夠直接操作對象欄位 那麼就可以反射進行原子操作 但是對於父類的欄位 子類是不能直接操作的 盡管子類可以訪問父類的欄位
( )只能是實例變數 不能是類變數 也就是說不能加static關鍵字
( )只能是可修改變數 不能使final變數 因為final的語義就是不可修改 實際上final的語義和volatile是有沖突的 這兩個關鍵字不能同時存在
( )對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long類型的欄位 不能修改其包裝類型(Integer/Long) 如果要修改包裝類型就需要使用AtomicReferenceFieldUpdater
在下面的例子中描述了操作的方法
[java]
import ncurrent atomic AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
class DemoData{
public volatile int value = ;
volatile int value = ;
protected volatile int value = ;
private volatile int value = ;
}
AtomicIntegerFieldUpdater<DemoData> getUpdater(String fieldName) {
return AtomicIntegerFieldUpdater newUpdater(DemoData class fieldName)
}
void doit() {
DemoData data = new DemoData()
System out println( ==> +getUpdater( value ) getAndSet(data ))
System out println( ==> +getUpdater( value ) incrementAndGet(data))
System out println( ==> +getUpdater( value ) decrementAndGet(data))
System out println( true ==> +getUpdater( value ) pareAndSet(data ))
}
public static void main(String[] args) {
AtomicIntegerFieldUpdaterDemo demo = new AtomicIntegerFieldUpdaterDemo()
demo doit()
}
}
在上面的例子中DemoData的欄位value /value 對於AtomicIntegerFieldUpdaterDemo類是不可見的 因此通過反射是不能直接修改其值的
AtomicMarkableReference類描述的一個<Object Boolean>的對 可以原子的修改Object或者Boolean的值 這種數據結構在一些緩存或者狀態描述中比較有用 這種結構在單個或者同時修改Object/Boolean的時候能夠有效的提高吞吐量
AtomicStampedReference類維護帶有整數 標志 的對象引用 可以用原子方式對其進行更新 對比AtomicMarkableReference類的<Object Boolean> AtomicStampedReference維護的是一種類似<Object int>的數據結構 其實就是對對象(引用)的一個並發計數 但是與AtomicInteger不同的是 此數據結構可以攜帶一個對象引用(Object) 並且能夠對此對象和計數同時進行原子操作
在本文結尾會提到 ABA問題 而AtomicMarkableReference/AtomicStampedReference在解決 ABA問題 上很有用
三 Atomic類的作用
使得讓對單一數據的操作 實現了原子化
使用Atomic類構建復雜的 無需阻塞的代碼
訪問對 個或 個以上的atomic變數(或者對單個atomic變數進行 次或 次以上的操作)通常認為是需要同步的 以達到讓這些操作能被作為一個原子單元
無鎖定且無等待演算法
基於 CAS (pare and swap)的並發演算法稱為 無鎖定演算法 因為線程不必再等待鎖定(有時稱為互斥或關鍵部分 這取決於線程平台的術語) 無論 CAS 操作成功還是失敗 在任何一種情況中 它都在可預知的時間內完成 如果 CAS 失敗 調用者可以重試 CAS 操作或採取其他適合的操作
如果每個線程在其他線程任意延遲(或甚至失敗)時都將持續進行操作 就可以說該演算法是 無等待的 與此形成對比的是 無鎖定演算法要求僅 某個線程總是執行操作 (無等待的另一種定義是保證每個線程在其有限的步驟中正確計算自己的操作 而不管其他線程的操作 計時 交叉或速度 這一限制可以是系統中線程數的函數 例如 如果有 個線程 每個線程都執行一次CasCounter increment() 操作 最壞的情況下 每個線程將必須重試最多九次 才能完成增加 )
再過去的 年裡 人們已經對無等待且無鎖定演算法(也稱為 無阻塞演算法)進行了大量研究 許多人通用數據結構已經發現了無阻塞演算法 無阻塞演算法被廣泛用於操作系統和 JVM 級別 進行諸如線程和進程調度等任務 雖然它們的實現比較復雜 但相對於基於鎖定的備選演算法 它們有許多優點 可以避免優先順序倒置和死鎖等危險 競爭比較便宜 協調發生在更細的粒度級別 允許更高程度的並行機制等等
常見的
非阻塞的計數器Counter
非阻塞堆棧ConcurrentStack
lishixin/Article/program/Java/gj/201311/27474
6. 什麼是原子操作,java中的原子操作是什麼
"原子操作(atomic
operation)是不需要synchronized",這是Java多線程編程的老生常談了。所謂原子操作是指不會被鉛余讓線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何
context
switch
(切[1]
換到毀鎮另一個線槐局程)。
7. 原子操作的實現原理
我們一起來聊一聊在Inter處理器和Java里是如何實現原子操作的。
32位IA-32處理器使用基於 對緩存加鎖或匯流排加鎖 的方式來實現多處理器之間的原子操作
首先處理器會自動保證基本的內存操作的原子性。 處理器保證從系統內存當中讀取或者寫入一個位元組是原子的,意思是當一個處理器讀取一個位元組時,其他處理器不能訪問這個位元組的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內存操作處理器不能自動保證其原子性,比如跨匯流排寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供匯流排鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。
第一個機制是通過匯流排鎖保證原子性。 如果多個處理器同時對共享變數進行讀改寫(i++就是經典的讀改寫操作)操作,那麼共享變數就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變數的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。如下圖
處理器使用匯流排鎖就是來解決這個問題的。 所謂匯流排鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在匯流排上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨占使用共享內存。
「緩存鎖定」指內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到內存時,處理器不需要在匯流排上聲言LOCK#信號,而是修改內部的內存地址,通過緩存一致性機制保證操作的原子性。
例外:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行,處理器會調用匯流排鎖定。
在java中可以通過鎖和循環CAS的方式來實現原子操作。
CAS
ABA問題
循環時間長開銷大
只能保證一個共享變數的原子操作
原子操作的實現原理
聊聊並發(五)原子操作的實現原理
8. 看了這篇文章,你還敢說你了解volatile關鍵字嗎
想要理解volatile為什麼能確保可見性,就要先理解Java中的內存模型是什麼樣的。
Java內存模型規定了 所有的變數都存儲在主內存中 。 每條線程中還有自己的工作內存,線程的工作內存中保存了被該線程所使用到的變數(這些變數是從主內存中拷貝而來) 。 線程對變數的所有操作(讀取,賦值)都必須在工作內存中進行。不同線程之間也無法直接訪問對方工作內存中的變數,線程間變數值的傳遞均需要通過主內存來完成 。
基於此種內存模型,便產生了多線程編程中的數據「臟讀」等問題。
舉個簡單的例子:在java中,執行下面這個語句:
i = 10;
執行線程必須先在自己的工作線程中對變數i所在的緩存行進行賦值操作,然後再寫入主存當中。而不是直接將數值10寫入主存當中。
比如同時有2個線程執行這段代碼,假如初始時i的值為10,那麼我們希望兩個線程執行完之後i的值變為12。但是事實會是這樣嗎?
可能存在下面一種情況:初始時,兩個線程分別讀取i的值存入各自所在的工作內存當中,然後線程1進行加1操作,然後把i的最新值11寫入到內存。此時線程2的工作內存當中i的值還是10,進行加1操作之後,i的值為11,然後線程2把i的值寫入內存。
最終結果i的值是11,而不是12。這就是著名的緩存一致性問題。通常稱這種被多個線程訪問的變數為共享變數。
那麼如何確保共享變數在多線程訪問時能夠正確輸出結果呢?
在解決這個問題之前,我們要先了解並發編程的三大概念: 原子性,有序性,可見性 。
1.定義
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
2.實例
一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個操作不具備原子性,會造成什麼樣的後果。假如從賬戶A減去1000元之後,操作突然中止。這樣就會導致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個轉過來的1000元。
所以這2個操作必須要具備原子性才能保證不出現一些意外的問題。
同樣地反映到並發編程中會出現什麼結果呢?
舉個最簡單的例子,大家想一下假如為一個32位的變數賦值過程不具備原子性的話,會發生什麼後果?
假若一個線程執行到這個語句時,我暫且假設為一個32位的變數賦值包括兩個過程:為低16位賦值,為高16位賦值。
那麼就可能發生一種情況:當將低16位數值寫入之後,突然被中斷,而此時又有一個線程去讀取i的值,那麼讀取到的就是錯誤的數據。
3.Java中的原子性
在Java中, 對基本數據類型的變數的讀取和賦值操作是原子性操作 ,即這些操作是不可被中斷的,要麼執行,要麼不執行。
上面一句話雖然看起來簡單,但是理解起來並不是那麼容易。看下面一個例子i:
請分析以下哪些操作是原子性操作:
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
咋一看,可能會說上面的4個語句中的操作都是原子性操作。其實只有語句1是原子性操作,其他三個語句都不是原子性操作。
語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工作內存中。
語句2實際上包含2個操作,它先要去讀取x的值,再將x的值寫入工作內存 ,雖然讀取x的值以及 將x的值寫入工作內存 這2個操作都是原子性操作,但是合起來就不是原子性操作了。
同樣的, x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值 。
所以上面4個語句只有語句1的操作具備原子性。
也就是說, 只有簡單的讀取、賦值(而且必須是將數字賦值給某個變數,變數之間的相互賦值不是原子操作)才是原子操作。
從上面可以看出,Java內存模型只保證了基本讀取和賦值是原子性操作, 如果要實現更大范圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。
1.定義
可見性是指當多個線程訪問同一個變數時,一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值。
2.實例
舉個簡單的例子,看下面這段代碼:
//線程1執行的代碼
int i = 0;
i = 10;
//線程2執行的代碼
j = i;
由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值載入到工作內存中,然後賦值為10,那麼在線程1的工作內存當中i的值變為10了,卻沒有立即寫入到主存當中。
此時線程2執行 j = i,它會先去主存讀取i的值並載入到線程2的工作內存當中,注意此時內存當中i的值還是0,那麼就會使得j的值為0,而不是10.
這就是可見性問題,線程1對變數i修改了之後,線程2沒有立即看到線程1修改的值。
3.Java中的可見性
對於可見性,Java提供了volatile關鍵字來保證可見性。
當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
而普通的共享變數不能保證可見性, 因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且 在釋放鎖之前會將對變數的修改刷新到主存當中 。因此可以保證可見性。
1.定義
有序性:即程序執行的順序按照代碼的先後順序執行。
2.實例
舉個簡單的例子,看下面這段代碼:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個int型變數,定義了一個boolean類型變數,然後分別對兩個變數進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那麼JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,為什麼呢?這里可能會發生指令重排序(Instruction Reorder)。
下面解釋一下什麼是指令重排序, 一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。
但是要注意,雖然處理器會對指令進行重排序,但是它會保證程序最終結果會和代碼順序執行結果相同,那麼它靠什麼保證的呢?再看下面一個例子:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
這段代碼有4個語句,那麼可能的一個執行順序是:
那麼可不可能是這個執行順序呢: 語句2 語句1 語句4 語句3
不可能,因為處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。
雖然重排序不會影響單個線程內程序執行的結果,但是多線程呢?下面看一個例子:
上面代碼中,由於語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以為初始化工作已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並沒有被初始化,就會導致程序出錯。
從上面可以看出, 指令重排序不會影響單個線程的執行,但是會影響到線程並發執行的正確性。
也就是說, 要想並發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
3.Java中的有序性
在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。
在Java裡面,可以通過volatile關鍵字來保證一定的「有序性」。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。
另外,Java內存模型具備一些先天的「有序性」, 即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下面就來具體介紹下happens-before原則(先行發生原則):
①程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
②鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
③volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
④傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
⑤線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
⑥線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
⑦線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
⑧對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
這8條規則中,前4條規則是比較重要的,後4條規則都是顯而易見的。
下面我們來解釋一下前4條規則:
對於程序次序規則來說,就是一段程序代碼的執行 在單個線程中看起來是有序的 。注意,雖然這條規則中提到「書寫在前面的操作先行發生於書寫在後面的操作」,這個應該是程序看起來執行的順序是按照代碼順序執行的, 但是虛擬機可能會對程序代碼進行指令重排序 。雖然進行重排序,但是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此, 在單個線程中,程序執行看起來是有序執行的 ,這一點要注意理解。事實上, 這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。
第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中, 同一個鎖如果處於被鎖定的狀態,那麼必須先對鎖進行了釋放操作,後面才能繼續進行lock操作。
第三條規則是一條比較重要的規則。直觀地解釋就是, 如果一個線程先去寫一個變數,然後一個線程去進行讀取,那麼寫入操作肯定會先行發生於讀操作。
第四條規則實際上就是體現happens-before原則 具備傳遞性 。
1.volatile保證可見性
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了 不同線程對這個變數進行操作時的可見性 ,即一個線程修改了某個變數的值,這新值對其他線程來說是立即可見的。
2) 禁止進行指令重排序。
先看一段代碼,假如線程1先執行,線程2後執行:
這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會採用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發生這種情況就會造成死循環了)。
下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經解釋過,每個線程在運行過程中都有自己的工作內存,那麼線程1在運行的時候,會將stop變數的值拷貝一份放在自己的工作內存當中。
那麼當線程2更改了stop變數的值之後,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那麼線程1由於不知道線程2對stop變數的更改,因此還會一直循環下去。
但是用volatile修飾之後就變得不一樣了:
第一:使用volatile關鍵字會 強制將修改的值立即寫入主存 ;
第二:使用volatile關鍵字的話,當線程2進行修改時, 會導致線程1的工作內存中緩存變數stop的緩存行無效 (反映到硬體層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
第三:由於線程1的工作內存中緩存變數stop的緩存行無效,所以 線程1再次讀取變數stop的值時會去主存讀取 。
那麼在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內存中的值,然後將修改後的值寫入內存),會使得線程1的工作內存中緩存變數stop的緩存行無效,然後線程1讀取時,發現自己的緩存行無效,它會等待緩存行對應的主存地址被更新之後,然後去對應的主存讀取最新的值。
那麼線程1讀取到的就是最新的正確的值。
2.volatile不能確保原子性
下面看一個例子:
大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
可能有的朋友就會有疑問,不對啊,上面是對變數inc進行自增操作,由於volatile保證了可見性,那麼在每個線程中對inc自增完之後,在其他線程中都能看到修改後的值啊,所以有10個線程分別進行了1000次操作,那麼最終inc的值應該是1000*10=10000。
這裡面就有一個誤區了, volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。 可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變數的操作的原子性。
在前面已經提到過, 自增操作是不具備原子性的,它包括讀取變數的原始值、進行加1操作、寫入工作內存 。那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
假如某個時刻變數inc的值為10,
線程1對變數進行自增操作,線程1先讀取了變數inc的原始值,然後線程1被阻塞了 ;
然後線程2對變數進行自增操作,線程2也去讀取變數inc的原始值, 由於線程1隻是對變數inc進行讀取操作,而沒有對變數進行修改操作,所以不會導致線程2的工作內存中緩存變數inc的緩存行無效,也不會導致主存中的值刷新, 所以線程2會直接去主存讀取inc的值,發現inc的值時10,然後進行加1操作,並把11寫入工作內存,最後寫入主存。
然後線程1接著進行加1操作,由於已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然為10,所以線程1對inc進行加1操作後inc的值為11,然後將11寫入工作內存,最後寫入主存。
那麼兩個線程分別進行了一次自增操作後,inc只增加了1。
根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變數的任何操作都是原子性的。
解決方案:可以通過synchronized或lock,進行加鎖,來保證操作的原子性。也可以通過AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些 原子操作類 ,即對基本數據類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。 atomic是利用CAS來實現原子性操作的(Compare And Swap) ,CAS實際上是 利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操作。
3.volatile保證有序性
在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關鍵字禁止指令重排序有兩層意思:
1)當程序執行到volatile變數的讀操作或者寫操作時, 在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行 ;
2)在進行指令優化時, 不能將在對volatile變數的讀操作或者寫操作的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。
可能上面說的比較繞,舉個簡單的例子:
由於 flag變數為volatile變數 ,那麼在進行指令重排序的過程的時候, 不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
並且volatile關鍵字能保證, 執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。
那麼我們回到前面舉的一個例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
前面舉這個例子的時候,提到有可能語句2會在語句1之前執行,那麼久可能導致context還沒被初始化,而線程2中就使用未初始化的context去進行操作,導致程序出錯。
這里如果用volatile關鍵字對inited變數進行修飾,就不會出現這種問題了, 因為當執行到語句2時,必定能保證context已經初始化完畢。
1.可見性
處理器為了提高處理速度,不直接和內存進行通訊,而是將系統內存的數據獨到內部緩存後再進行操作,但操作完後不知什麼時候會寫到內存。
2.有序性
Lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),它確保 指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面; 即在執行到內存屏障這句指令時,在它前面的操作已經全部完成。
synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:
1)對變數的寫操作不依賴於當前值
2)該變數沒有包含在具有其他變數的不變式中
下面列舉幾個Java中使用volatile的幾個場景。
①.狀態標記量
volatile boolean flag = false;
//線程1
while(!flag){
doSomething();
}
//線程2
public void setFlag() {
flag = true;
}
根據狀態標記,終止線程。
②.單例模式中的double check
為什麼要使用volatile 修飾instance?
主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【 包括高級UI、性能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的復習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
9. Java中如何實現原子操作
Java中的原子操作包括:
1)除long和double之寬帆外的基本類型的賦值操作
2)所有引用reference的賦值操作
3)java.concurrent.Atomic.* 包中所有類的一切操作
count++不是原子操作,是3個原子操作組合
1.讀取主存中的count值,賦值給一個局部成員變數tmp
2.tmp+1
3.將tmp賦值給count
可能會出現線程1運行到第2步的時候,tmp值為1;這時CPU調度切換到線程2執行完畢,count值為1;切換到線程1,繼續執行第3步,count被賦值為1------------結果就是兩個線程執行完畢,count的值只加了1;
還有一點要注意,如果使用AtomicInteger.set(AtomicInteger.get() + 1),敏戚會和上述情況一樣有並發問題,要使用AtomicInteger.getAndIncrement()才可以橋巧陵避免並發問題