【併發程式設計】Java中的原子操作
什麼是原子操作
原子操作是指一個或者多個不可再分割的操作。這些操作的執行順序不能被打亂,這些步驟也不可以被切割而只執行其中的一部分(不可中斷性)。舉個列子:
//就是一個原子操作
int i = 1;
//非原子操作,i++是一個多步操作,而且是可以被中斷的。
//i++可以被分割成3步,第一步讀取i的值,第二步計算i+1;第三部將最終值賦值給i
i++;
Java中的原子操作
在Java中,我們可以通過同步鎖或者CAS操作來實現原子操作。
CAS操作
CAS是Compare and swap的簡稱,這個操作是硬體級別的操作,在硬體層面保證了操作的原子性。CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。Java中的sun.misc.Unsafe
compareAndSwapInt
和compareAndSwapLong
等幾個方法實現CAS。
另外,在jdk的atomic包下面提供了很多基於CAS實現的原子操作類,見下圖:
下面我們就使用其中的AtomicInteger
來看看怎麼使用這些原子操作類。
package com.csx.demo.spring.boot.concurrent.atomic; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerDemo { private static int THREAD_COUNT = 100; public static void main(String[] args) throws InterruptedException { NormalCounter normalCounter = new NormalCounter("normalCounter",0); SafeCounter safeCounter = new SafeCounter("safeCounter",0); List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < THREAD_COUNT ; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { normalCounter.add(1); safeCounter.add(1); } } }); threadList.add(thread); } for (Thread thread : threadList) { thread.start(); } for (Thread thread : threadList) { thread.join(); } System.out.println("normalCounter:"+normalCounter.getCount()); System.out.println("safeCounter:"+safeCounter.getCount()); } public static class NormalCounter{ private String name; private Integer count; public NormalCounter(String name, Integer count) { this.name = name; this.count = count; } public void add(int delta){ this.count = count+delta; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } } public static class SafeCounter{ private String name; private AtomicInteger count; public SafeCounter(String name, Integer count) { this.name = name; this.count = new AtomicInteger(count); } public void add(int delta){ count.addAndGet(delta); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getCount() { return count.get(); } public void setCount(Integer count) { this.count.set(count); } } }
上面的程式碼中,我們分別建立了一個普通的計數器和一個原子操作的計數器(使用AtomicInteger進行計數)。然後建立了100個執行緒,每個執行緒進行10000次計數。理論上執行緒執行完之後,計數器的值都是1000000,但是結果如下:
normalCounter:496527
safeCounter:1000000
每次執行,普通計數器的值都是不一樣的,而使用AtomicInteger進行計數的計數器都是1000000。
CAS操作存在的問題
- ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
迴圈時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
只能保證一個共享變數的原子操作:當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。
使用鎖來保證原子操作
還是以上面的列子為列,普通的計數器我們只需要在計數方法上加鎖就行了:
public synchronized void add(int delta){
this.count = count+delta;
}
執行結果如下:
normalCounter:1000000
safeCounter:1000000
兩個計數器都能拿到正確的結果