JAVA鎖原理之 CAS原子操做篇

原子操做是什麼?


原子操做(atomic operation)指的是由多步操做組成的一個操做。若是該操做不能原子地執行,則要麼執行完全部步驟,要麼一步也不執行,不可能只執行全部步驟的一個子集。java

現代操做系統中,通常都提供了原子操做來實現一些同步操做,所謂原子操做,也就是一個獨立而不可分割的操做。在單核環境中,通常的意義下原子操做中線程不會被切換,線程切換要麼在原子操做以前,要麼在原子操做完成以後。更普遍的意義下原子操做是指一系列必須總體完成的操做步驟,若是任何一步操做沒有完成,那麼全部完成的步驟都必須回滾,這樣就能夠保證要麼全部操做步驟都未完成,要麼全部操做步驟都被完成。mysql

例如在單核系統裏,單個的機器指令能夠當作是原子操做(若是有編譯器優化、亂序執行等狀況除外);在多核系統中,單個的機器指令就不是原子操做,由於多核系統裏是多指令流並行運行的,一個核在執行一個指令時,其餘核同時執行的指令有可能操做同一塊內存區域,從而出現數據競爭現象。多核系統中的原子操做一般使用內存柵障(memory barrier)來實現,即一個CPU核在執行原子操做時,其餘CPU核必須中止對內存操做或者不對指定的內存進行操做,這樣才能避免數據競爭問題算法

CAS是什麼?


CAS全程爲Compare and Swap即比較再交換sql

jdk5增長了併發包java.util.concurrent簡稱JUC,其下面的類使用CAS算法實現了區別於synchronized同步鎖的一種樂觀鎖。JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這是一種排斥鎖也是悲觀鎖安全

線程獨享和線程共享及java的鎖種類


某個資源只給一個線程享用稱之爲線程獨享反之爲線程共享多線程

在平常開發時,須要肯定好哪些資源是線程共享的,共享的場景是什麼.才能更好的去使用不一樣的鎖策略,保證對資源操做的原子性.併發

java的鎖種類分爲8種高併發

  • 公平鎖/非公平鎖
  • 可重入鎖
  • 獨享鎖/共享鎖
  • 互斥鎖/讀寫鎖
  • 樂觀鎖/悲觀鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

不具有原子性操做的例子


資源類Resources性能

class Resources {
    
    private volatile int i = 0;
    
    public void add() {
        i++;
    }
    
    public int getI() {
        return i;
    }
}
複製代碼

測試類Demo測試

public class Demo {

    public static void main(String[] args) {
        // 資源類實例
        final Resources resources = new Resources();

        // 定長線程池-線程數10個
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 循環打印-啓動10個線程
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    // 每一個線程對資源類的i進行+1
                    for (int i1 = 0; i1 < 1000; i1++) {
                        resources.add();
                    }
                }
            });
        }

        // 阻塞主線程 - 等待線程池執行完畢
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 打印i的值,預期值應該是 10*1000*1 = 10000 纔算合理
        System.out.println("I的值打印爲:"+resources.getI());
    }
}
複製代碼

運行結果爲:I的值打印爲:8939

運行結果中獲得一個結論,多線程對Resources資源類中的add()方法進行i++,是線程不安全的。

有的小夥伴就很奇怪了,只是一行i++代碼爲何會是不安全的呢?不是說多步操做因爲CPU多核同時運行纔會不安全嗎?可i++明明就一行代碼啊

其實我一開始也是這麼認爲的,爲何一行代碼會出現線程不安全的問題,因而帶着疑問反編譯了java代碼後發現i++是一行代碼可是卻不是一步操做.反編譯命令:javap -c Resources.class

反編譯後的Resources代碼以下:

class com.cas.Resources {
  com.cas.Resources();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field i:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field i:I
      10: return

  public int getI();
    Code:
       0: aload_0
       1: getfield      #2                  // Field i:I
       4: ireturn
}
複製代碼

仔細觀察add方法翻譯JVM指令以下

  • aload_0 : 從局部變量表的相應位置裝載一個對象引用到操做數棧的棧頂
  • dup : 表示數據重複定義,也就是複製操做數
  • getfield : 獲取I存放在堆的值
  • iconst_1 : 當int取值1~5時,JVM採用iconst指令將常量壓入棧中
  • iadd : 計算i的值,說白了就是i+1
  • putfield : 將計算I的結果寫到堆內存中
  • return : 結束方法

從字節碼指令看出,i++其實是通過3個主要步驟

  • 從堆內存中獲取到i的值而且複製一份到當前線程的操做數棧中
  • 從當前線程的操做數棧中去計算i的值
  • 計算後結果寫回到堆內存中的i去

那麼基本上能夠肯定不安全的點在於每一個線程預先保留好從堆內存中獲取到的i值到操做數棧(線程臨時存儲區),而後將操做數棧的值計算後再寫回到堆內存中。這樣的過程就會發生數據髒讀的問題了

以下圖:

image-20200113161836693

從圖中展現若是兩個線程都對當前線程的操做數棧中的變量i進行+1,致使明明兩個線程共加了兩次結果卻只加了1

JUC中的原子性操做類

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

這裏就舉例一些經常使用的幾個類,想要了解更多的朋友們可查看JDK中java.util.concurrent.atomic包下的類,此包下全部的類都是原子性操做的

image-20200113163221637

本文將使用AtomicInteger這個號稱保證線程安全的int類型原子包裝類進行一次測試

資源類稍微作下修改,將int i 聲明爲AtomicInteger i

資源類

class Resources {

    private volatile AtomicInteger i = new AtomicInteger(0);

    public void add() {
        i.getAndAdd(1);
    }

    public int getI() {
        return i.get();
    }
}
複製代碼

運行結果爲:I的值打印爲:10000

很顯然,當咱們使用AtomicInteger進行增長的時候,i在多線程的操做下準確的計算到了10000,這個值是正確的。可是爲何AtomicInteger能讓i線程安全呢?會不會是使用了synchronized關鍵字呢?讓咱們深刻一探究竟

image-20200113164412989

image-20200113165044633

點進去發現只用了一行代碼搞定,並且尚未鎖相關代碼,可是調用了一個方法,不會是那個方法加了鎖吧,來猜想觀察一下調用getAndAddInt方法的三個傳參是什麼意思

unsafe.getAndAddInt(this, valueOffset, delta)
// this 當前對象
// valueOffset 是什麼鬼?
// delta 須要增長的值
// unsafe 這個又是什麼鬼?這個相似乎沒見過啊
複製代碼

image-20200113170054551

那咱們再點進去看一下unsafe.getAndAddInt(this, valueOffset, delta)究竟作了什麼,即便是直接操做內存那也不能保證線程安全啊

image-20200113170922327

看完嚇一跳,寫個where循環在裏面調用compareAndSwapInt(var1, var2, var5, var5 + var4)方法,那能猜測到的應該是這個線程去嘗試着搶鎖,有可能搶不到,而後掛起當前線程不停去自旋搶鎖直到搶到鎖而且增長成功爲止

先分析這段代碼

// var1 當前對象
// var2 當前對象值的位移量
// var4 須要增長的值
// var5 聲明他做甚?
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

// 再看看這行代碼
// var5 = this.getIntVolatile(var1, var2); 根據當前對象和當前對象值的位移量 獲取內存中最新的值
// 再往下看看這行代碼
// compareAndSwapInt(var1, var2, var5, var5 + var4) 再想一想cas的全稱即比較再交換

// var1 當前對象
// var2 當前對象值的位移量
// var5 當前對象在內存中最新的值
// (var5 + var4) 換成計算後的值
複製代碼

原來如此,這像不像mysql中innodb的行鎖特性

update stu set status=2 where id = 1 and status=1
// 經過ID肯定好索引
// 經過索引鎖定數據再判斷status=1 
// 纔將id爲1的行修改爲status=2
複製代碼

得出結論,這就是cas實現樂觀鎖的方式,先比較再進行賦值

CAS算法的好處


CAS是一種無鎖算法,經過硬件層面上對前後操做內存的線程進行排隊處理CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

CAS(比較並交換)是CPU指令級的操做,只有一步原子操做,因此很是快。並且避免了請求操做系統來裁定鎖的問題,不用麻煩操做系統,直接在CPU內部就搞定了

CAS算法的弊端


剛剛看到一個問題,若是增長不成功,那while會一直嘗試着去增長,會不會產生死鎖或致使線程耗時過久的一系列問題發生啊?

  • 高併發且自旋不成時

    自旋若是長時間不成功,會帶來很大的性能開銷。若是變動操做很耗時,同時變動很頻繁,就可能致使自旋長時間不成功,帶來大量的性能開銷

手寫一個簡單的鎖


public class MyLock implements Lock {

    /** * 鎖的擁有者 */
    private AtomicReference<Thread> atomicReference = new AtomicReference();

    /** * 線程等待隊列 */
    private LinkedBlockingQueue<Thread> linkedBlockingQueue = new LinkedBlockingQueue<Thread>();

    /** * 加鎖 */
    public void lock() {
        if (!tryLock()) {
            // 若是搶鎖失敗,將線程進入等待隊列,並掛起當前線程
            linkedBlockingQueue.offer(Thread.currentThread());

            // 掛起當前線程方式有 suspend park wait
            // suspend已被棄用了,wait必須配合synchronized才能使用,因此合適咱們的也只有park了

            // 線程之間的喚醒有多是僞喚醒,因此須要寫死循環
            while (true) {
                // 只有隊列頭部的線程等於當前線程才進行搶鎖,不然掛起
                if (linkedBlockingQueue.peek() == Thread.currentThread()) {
                    // 搶不到鎖就掛起,搶到鎖出隊列而且退出lock方法
                    if (!tryLock()) {
                        LockSupport.park();
                    } else {
                        linkedBlockingQueue.poll();
                        return;
                    }
                } else {
                    LockSupport.park();
                }
            }
        }
    }

    /** * 嘗試搶鎖 */
    public boolean tryLock() {
        // 若是鎖的擁有者爲null,那就將他設爲當前線程
        return atomicReference.compareAndSet(null, Thread.currentThread());
    }

    /** * 釋放鎖 */
    public void unlock() {
        // 嘗試釋放鎖成功後-喚醒隊列頭部線程
        if (atomicReference.compareAndSet(Thread.currentThread(), null)) {
            Thread peek = linkedBlockingQueue.peek();
            if (peek != null) {
                LockSupport.unpark(peek);
            }
        }
    }


    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public Condition newCondition() {
        return null;
    }
}
複製代碼

測試:資源類稍微作下修改,將AtomicInteger i 聲明爲int i

class Resources {
    /** * 自定義鎖 */
    private MyLock myLock = new MyLock();

    /** * 非原子操做的int類型 */
    private volatile int i = 0;

    public void add() {
        myLock.lock();
        try {
            i++;
        } finally {
            myLock.unlock();
        }
    }

    public int getI() {
        return i;
    }
}
複製代碼

運行結果爲:I的值打印爲:10000

運行結果是對的,經過MyLock簡單實現CAS的例子,應該對CAS算法的理解更加深入

相關文章
相關標籤/搜索