JAVA之原子操做原理

咱們在解決併發問題時,不少時候都用到java的java.util.concurrent.atomic包,那麼其中原理是什麼?java

1、緒論

Java中原子操做是依賴於處理器實現的,處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。編程

  • 使用總線鎖保證原子性。 所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔共享內存。
  • 使用緩存鎖保證原子性。 所謂的緩存鎖定是指內存區域若是被緩存在處理器的緩存行中,而且在Lock操做期間被鎖定,那麼當它執行鎖操做回寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並容許它的緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改兩個以上處理器緩存的內存區域數據,當其餘處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。

問題:爲何要引入緩存鎖定? 由於在某些狀況下,咱們須要保證對某個內存地址的操做是原子性便可,可是總線鎖定把SPU和內存之間的通信鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。 緩存

2、Java實現原子操做介紹

在Java中能夠經過鎖和循環CAS的方式來實現原子操做。CAS原理在上一篇《JAVA之鎖機制實現原理(簡化版)》介紹偏向鎖中有提到。安全

1.使用循環CAS實現原子操做。

下面來看一個基於CAS線程安全的計數器方法safeCount和非安全的計數器count的例子。bash

public class CasAtomicInteger {
    private int i = 0;
    private AtomicInteger atomicI = new AtomicInteger(0);
    public static void main(String[] args) {
        final CasAtomicInteger cas = new CasAtomicInteger();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j ++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10000; i ++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }

        for (Thread t : ts) {
            t.start();
        }

        for (Thread t : ts) {
            try {
                t.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.out.println("非線程安全count計數器:" + cas.i);
        System.out.println("線程安全safeCount計數器:" + cas.atomicI.get());
        System.out.println(System.currentTimeMillis() - start);
    }

    /** * 使用CAS實現線程安全計數器 */
    private void safeCount() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }

    /** * 非線程安全計數器 */
    private void count() {
        i++;
    }
}
複製代碼

數據結果:併發

非線程安全count計數器:998883
線程安全safeCount計數器:1000000
107
複製代碼

顯然,非線程安全count計數器少了計數。在此,咱們遇到相似的併發問題能夠用到java的java.util.concurrent.atomic包裏的工具解決。atomic包提供了一些原子操做,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)、AtomicLong(用原子方式更新的long值)等。工具

二、淺談CAS原子操做的三大問題

(1) ABA問題。 由於CAS須要在操做值的時候,檢查值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變回了A,那麼使用CAS進行檢查時會發現它的值沒有變化,可是其實是變化了。post

解決思路:使用版本號,每次變量更新的時候把版本號加1.從Java1.5開始,JDK的Atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的做用是首先檢查當前引用是否等於預期引用,而且檢查當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。 優化

(2) 循環時間長開銷大。 自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令,那麼效率會有必定的提高。atom

pause指令有兩個做用:第一,它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零;第二,它能夠避免在退出循環的時候因內存順序衝突(Memory Order Violation)而引發CPU流水線被清空(CPU Pipeline Flush),從而提升CPU的執行效率。

(3)只能保證一個共享變量的原子操做。 當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖。

參考:《Java 併發編程的藝術》 方騰飛,魏鵬,程曉明

相關文章
相關標籤/搜索