這 20 多個高併發程式設計必備的知識點,你都會嗎?

架構文摘發表於2020-01-04

轉載自併發程式設計網 – ifeve.com

http://ifeve.com/%e9%ab%98%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%bf%85%e5%a4%87%e5%9f%ba%e7%a1%80/

一、前言

借用Java併發程式設計實踐中的話”編寫正確的程式並不容易,而編寫正常的併發程式就更難了”,相比於順序執行的情況,多執行緒的執行緒安全問題是微妙而且出乎意料的,因為在沒有進行適當同步的情況下多執行緒中各個操作的順序是不可預期的,本文算是對多執行緒情況下同步策略的一個簡單介紹。

二、 什麼是執行緒安全問題

執行緒安全問題是指當多個執行緒同時讀寫一個狀態變數,並且沒有任何同步措施時候,導致髒資料或者其他不可預見的結果的問題。Java中首要的同步策略是使用Synchronized關鍵字,它提供了可重入的獨佔鎖。

三、 什麼是共享變數可見性問題

要談可見性首先需要介紹下多執行緒處理共享變數時候的Java中記憶體模型。

Java記憶體模型規定了所有的變數都存放在主記憶體中,當執行緒使用變數時候都是把主記憶體裡面的變數拷貝到了自己的工作空間或者叫做工作記憶體。

當執行緒操作一個共享變數時候操作流程為:*

  • 執行緒首先從主記憶體拷貝共享變數到自己的工作空間
  • 然後對工作空間裡的變數進行處理
  • 處理完後更新變數值到主記憶體

那麼假如執行緒A和B同時去處理一個共享變數,會出現什麼情況那?

首先他們都會去走上面的三個流程,假如執行緒A拷貝共享變數到了工作記憶體,並且已經對資料進行了更新但是還沒有更新會主記憶體(結果可能目前存放在當前cpu的暫存器或者快取記憶體),這時候執行緒B拷貝共享變數到了自己的工作記憶體進行處理,處理後,執行緒A才把自己的處理結果更更新到主記憶體或者快取,可知 執行緒B處理的並不是執行緒A處理後的結果,也就是說執行緒A處理後的變數值對執行緒B不可見,這就是共享變數的不可見性問題。

構成共享變數記憶體不可見原因是因為三步流程不是原子性操作,下面知道使用恰當同步就可以解決這個問題。

我們知道ArrayList是執行緒不安全的,因為他的讀寫方法沒有同步策略,會導致髒資料和不可預期的結果,下面我們就一一講解如何解決。

這是執行緒不安全的
public class ArrayList<E> 
{

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
}複製程式碼

四、原子性

4.1 介紹

假設執行緒A執行操作Ao和執行緒B執行操作Bo ,那麼從A看,當B執行緒執行Bo操作時候,那麼Bo操作全部執行,要麼全部不執行,我們稱Ao和Bo操作互為原子性操作,在設計計數器時候一般都是先讀取當前值,然後+1,然後更新會變數,是讀-改-寫的過程,這個過程必須是原子性的操作。

public class ThreadNotSafeCount {

    private  Long value;

    public Long getCount() {
        return value;
    }

    public void inc() {
        ++value;
    }
}複製程式碼

如上程式碼是執行緒不安全的,因為不能保證++value是原子性操作。方法一是使用Synchronized進行同步如下:

public class ThreadSafeCount {

    private  Long value;

    public synchronized Long getCount() {
        return value;
    }

    public synchronized void inc() {
        ++value;
    }
}複製程式碼

注意: 這裡不能簡單的使用volatile修飾value進行同步,因為變數值依賴了當前值

使用Synchronized確實可以實現執行緒安全,即實現可見性和同步,但是Synchronized是獨佔鎖,沒有獲取內部鎖的執行緒會被阻塞掉,那麼有沒有剛好的實現那?答案是肯定的。

4.2 原子變數類

原子變數類比鎖更輕巧,比如AtomicLong代表了一個Long值,並提供了get,set方法,get,set方法語義和volatile相同,因為AtomicLong內部就是使用了volatile修飾的真正的Long變數。另外提供了原子性的自增自減操作,所以計數器可以改下為:

public class ThreadSafeCount {

    private  AtomicLong value = new AtomicLong(0L);

    public  Long getCount() {
        return value.get();
    }

    public void inc() {
        value.incrementAndGet();
    }
}複製程式碼

那麼相比使用synchronized的好處在於原子類操作不會導致執行緒的掛起和重新排程,因為他內部使用的是cas的非阻塞演算法。

常用的原子類變數為:

  • AtomicLong
  • AtomicInteger
  • AtomicBoolean
  • AtomicReference

五、CAS介紹

CAS 即CompareAndSet,也就是比較並設定,CAS有三個運算元分別為:記憶體位置,舊的預期值,新的值,操作含義是當記憶體位置的變數值為舊的預期值時候使用新的值替換舊的值。通俗的說就是看記憶體位置的變數值是不是我給的舊的預期值,如果是則使用我給的新的值替換他,如果不是返回給我舊值。這個是處理器提供的一個原子性指令。上面介紹的AtomicLong的自增就是使用這種方式實現:

public final long incrementAndGet() {
    for (;;) {
        long current = get();(1)
        long next = current + 1;(2)
        if (compareAndSet(current, next))(3)
            return next;
    }
}

public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}複製程式碼

假如當前值為1,那麼執行緒A和檢查B同時執行到了(3)時候各自的next都是2,current=1,假如執行緒A先執行了3,那麼這個是原子性操作,會把檔期值更新為2並且返回1,if判斷true所以incrementAndGet返回2.這時候執行緒B執行3,因為current=1而當前變數實際值為2,所以if判斷為false,繼續迴圈,如果沒有其他執行緒去自增變數的話,這次執行緒B就會更新變數為3然後退出。

這裡使用了無限迴圈使用CAS進行輪詢檢查,雖然一定程度浪費了cpu資源,但是相比鎖來說避免的執行緒上下文切換和排程。

六、什麼是可重入鎖

當一個執行緒要獲取一個被其他執行緒佔用的鎖時候,該執行緒會被阻塞,那麼當一個執行緒再次獲取它自己已經獲取的鎖時候是否會被阻塞那?如果不需要阻塞那麼我們說該鎖是可重入鎖,也就是說只要該執行緒獲取了該鎖,那麼可以無限制次數進入被該鎖鎖住的程式碼。

先看一個例子如果鎖不是可重入的,看看會出現什麼問題。

public class Hello{
     public Synchronized void helloA(){
        System.out.println("hello");
     }

     public Synchronized void helloB(){
        System.out.println("hello B");
        helloA();
     }

}複製程式碼

如上面程式碼當呼叫helloB函式前會先獲取內建鎖,然後列印輸出,然後呼叫helloA方法,呼叫前會先去獲取內建鎖,如果內建鎖不是可重入的那麼該呼叫就會導致死鎖了,因為執行緒持有並等待了鎖。

實際上內部鎖是可重入鎖,例如synchronized關鍵字管理的方法,可重入鎖的原理是在鎖內部維護了一個執行緒標示,標示該鎖目前被那個執行緒佔用,然後關聯一個計數器,一開始計數器值為0,說明該鎖沒有被任何執行緒佔用,當一個執行緒獲取了該鎖,計數器會變成1,其他執行緒在獲取該鎖時候發現鎖的所有者不是自己所以被阻塞,但是當獲取該鎖的執行緒再次獲取鎖時候發現鎖擁有者是自己會把計數器值+1, 當釋放鎖後計數器會-1,當計數器為0時候,鎖裡面的執行緒標示重置為null,這時候阻塞的執行緒會獲取被喚醒來獲取該鎖。

七、Synchronized關鍵字

7.1 Synchronized介紹

synchronized塊是Java提供的一種強制性內建鎖,每個Java物件都可以隱式的充當一個用於同步的鎖的功能,這些內建的鎖被稱為內部鎖或者叫監視器鎖,執行程式碼在進入synchronized程式碼塊前會自動獲取內部鎖,這時候其他執行緒訪問該同步程式碼塊時候會阻塞掉。拿到內部鎖的執行緒會在正常退出同步程式碼塊或者異常丟擲後釋放內部鎖,這時候阻塞掉的執行緒才能獲取內部鎖進入同步程式碼塊。

7.2 Synchronized同步例項

內部鎖是一種互斥鎖,具體說是同時只有一個執行緒可以拿到該鎖,當一個執行緒拿到該鎖並且沒有釋放的情況下,其他執行緒只能等待。

對於上面說的ArrayList可以使用synchronized進行同步來處理可見性問題。

使用synchronized對方法進行同步
public class ArrayList<E>
{

    public synchronized  E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    public synchronized E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
}複製程式碼

如圖當執行緒A獲取內部鎖進入同步程式碼塊後,執行緒B也準備要進入同步塊,但是由於A還沒釋放鎖,所以B現在進入等待,使用同步可以保證執行緒A獲取鎖到釋放鎖期間的變數值對B獲取鎖後都可見。也就是說當B開始執行A執行的程式碼同步塊時候可以看到A操作的所有變數值,這裡具體說是當執行緒B獲取b的值時候能夠保證獲取的值是2。這時因為執行緒A進入同步塊修改變數值後,會在退出同步塊前把值重新整理到主記憶體,而執行緒B在進入同步塊前會首先清空本地記憶體內容,從主記憶體重新獲取變數值,所以實現了可見性。但是要注意一點所有執行緒使用的是同一個鎖。

注意: Synchronized關鍵字會引起執行緒上下文切換和執行緒排程。

八、 ReentrantReadWriteLock介紹

使用synchronized可以實現同步,但是缺點是同時只有一個執行緒可以訪問共享變數,但是正常情況下,對於多個讀操作操作共享變數時候是不需要同步的,synchronized時候無法實現多個讀執行緒同時執行,而大部分情況下讀操作次數多於寫操作,所以這大大降低了併發性,所以出現了ReentrantReadWriteLock,它可以實現讀寫分離,多個執行緒同時進行讀取,但是最多一個寫執行緒存在。

對於上面的方法現在可以修改為:

public class ArrayList<E>
{
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  public E get(int index) {

        Lock readLock = readWriteLock.readLock();
        readLock.lock();
        try {
            return list.get(index);
        } finally {
            readLock.unlock();
        }
    }

    public E set(int index, E element) {

        Lock wirteLock = readWriteLock.writeLock();
        wirteLock.lock();
        try {
            return list.set(index, element);
        } finally {
            wirteLock.unlock();
        }
    }
}複製程式碼

如程式碼在get方法時候通過 readWriteLock.readLock()獲取了讀鎖,多個執行緒可以同時獲取這讀鎖,set方法通過readWriteLock.writeLock()獲取了寫鎖,同時只有一個執行緒可以獲取寫鎖,其他執行緒在獲取寫鎖時候會阻塞直到寫鎖被釋放。假如一個執行緒已經獲取了讀鎖,這時候如果一個執行緒要獲取寫鎖時候要等待直到釋放了讀鎖,如果一個執行緒獲取了寫鎖,那麼所有獲取讀鎖的執行緒需要等待直到寫鎖被釋放。所以相比synchronized來說執行多個讀者同時存在,所以提高了併發量。

注意: 需要使用者顯示呼叫Lock與unlock操作

九、 Volatile變數

對於避免不可見性問題,Java還提供了一種弱形式的同步,即使用了volatile關鍵字。該關鍵字確保了對一個變數的更新對其他執行緒可見。當一個變數被宣告為volatile時候,執行緒寫入時候不會把值快取在暫存器或者或者在其他地方,當執行緒讀取的時候會從主記憶體重新獲取最新值,而不是使用當前執行緒的拷貝記憶體變數值。

volatile雖然提供了可見性保證,但是不能使用他來構建複合的原子性操作,也就是說當一個變數依賴其他變數或者更新變數值時候新值依賴當前老值時候不在適用。

與synchronized相似之處在於如圖

如圖執行緒A修改了volatile變數b的值,然後執行緒B讀取了改變數值,那麼所有A執行緒在寫入變數b值前可見的變數值,在B讀取volatile變數b後對執行緒B都是可見的,圖中執行緒B對A操作的變數a,b的值都可見的。volatile的記憶體語義和synchronized有類似之處,具體說是說當執行緒寫入了volatile變數值就等價於執行緒退出synchronized同步塊(會把寫入到本地記憶體的變數值同步到主記憶體),讀取volatile變數值就相當於進入同步塊(會先清空本地記憶體變數值,從主記憶體獲取最新值)。

下面的Integer也是執行緒不安全的,因為沒有進行同步措施:

public class ThreadNotSafeInteger {

    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}複製程式碼

使用synchronized關鍵字進行同步如下:

public class ThreadSafeInteger {

    private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized  void set(int value) {
        this.value = value;
    }
}
複製程式碼

等價於使用volatile進行同步如下:

public class ThreadSafeInteger {

    private volatile int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}複製程式碼

這裡使用synchronized和使用volatile是等價的,但是並不是所有情況下都是等價,一般只有滿足下面所有條件才能使用volatile

  • 寫入變數值時候不依賴變數的當前值,或者能夠保證只有一個執行緒修改變數值。
  • 寫入的變數值不依賴其他變數的參與。
  • 讀取變數值時候不能因為其他原因進行加鎖。

另外 加鎖可以同時保證可見性和原子性,而volatile只保證變數值的可見性。

注意: volatile關鍵字不會引起執行緒上下文切換和執行緒排程。另外volatile還用來解決重排序問題,後面會講到。

十、 樂觀鎖與悲觀鎖

10.1 悲觀鎖

悲觀鎖,指資料被外界修改持保守態度(悲觀),在整個資料處理過程中,將資料處於鎖定狀態。 悲觀鎖的實現,往往依靠資料庫提供的鎖機制 。資料庫中實現是對資料記錄進行操作前,先給記錄加排它鎖,如果獲取鎖失敗,則說明資料正在被其他執行緒修改,則等待或者丟擲異常。如果加鎖成功,則獲取記錄,對其修改,然後事務提交後釋放排它鎖。

一個例子:select * from 表 where .. for update;複製程式碼

悲觀鎖是先加鎖再訪問策略,處理加鎖會讓資料庫產生額外的開銷,還有增加產生死鎖的機會,另外在多個執行緒只讀情況下不會產生資料不一致行問題,沒必要使用鎖,只會增加系統負載,降低併發性,因為當一個事務鎖定了該條記錄,其他讀該記錄的事務只能等待。

10.2 樂觀鎖

樂觀鎖是相對悲觀鎖來說的,它認為資料一般情況下不會造成衝突,所以在訪問記錄前不會加排他鎖,而是在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,具體說根據update返回的行數讓使用者決定如何去做。樂觀鎖並不會使用資料庫提供的鎖機制,一般在表新增version欄位或者使用業務狀態來做。

樂觀鎖直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。

十一、獨佔鎖與共享鎖

根據鎖能夠被單個執行緒還是多個執行緒共同持有,鎖又分為獨佔鎖和共享鎖。獨佔鎖保證任何時候都只有一個執行緒能讀寫許可權,ReentrantLock就是以獨佔方式實現的互斥鎖。共享鎖則可以同時有多個讀執行緒,但最多隻能有一個寫執行緒,讀和寫是互斥的,例如ReadWriteLock讀寫鎖,它允許一個資源可以被多執行緒同時進行讀操作,或者被一個執行緒 寫操作,但兩者不能同時進行。

獨佔鎖是一種悲觀鎖,每次訪問資源都先加上互斥鎖,這限制了併發性,因為讀操作並不會影響資料一致性,而獨佔鎖只允許同時一個執行緒讀取資料,其他執行緒必須等待當前執行緒釋放鎖才能進行讀取。

共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個執行緒同時進行讀操作。

十二、公平鎖與非公平鎖

根據執行緒獲取鎖的搶佔機制鎖可以分為公平鎖和非公平鎖,公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的時間多少來決定的,也就是最早加鎖的執行緒將最早獲取鎖,也就是先來先得的FIFO順序。而非公平鎖則執行闖入,也就是先來不一定先得。

ReentrantLock提供了公平和非公平鎖的實現:

  • 公平鎖ReentrantLock pairLock = new ReentrantLock(true);
  • 非公平鎖 ReentrantLock pairLock = new ReentrantLock(false);

如果建構函式不傳遞引數,則預設是非公平鎖。

在沒有公平性需求的前提下儘量使用非公平鎖,因為公平鎖會帶來效能開銷。假設執行緒A已經持有了鎖,這時候執行緒B請求該鎖將會被掛起,當執行緒A釋放鎖後,假如當前有執行緒C也需要獲取該鎖,如果採用非公平鎖方式,則根據執行緒排程策略執行緒B和C兩者之一可能獲取鎖,這時候不需要任何其他干涉,如果使用公平鎖則需要把C掛起,讓B獲取當前鎖。

十三、 AbstractQueuedSynchronizer介紹

AbstractQueuedSynchronizer提供了一個佇列,大多數開發者可能從來不會直接用到AQS,AQS有個變數用來存放狀態資訊 state,可以通過protected的getState,setState,compareAndSetState函式進行呼叫。對於ReentrantLock來說,state可以用來表示該執行緒獲可重入鎖的次數,semaphore來說state用來表示當前可用訊號的個數,FutuerTask用來表示任務狀態(例如還沒開始,執行,完成,取消)。

十四、CountDownLatch原理

14.1 一個例子

public class Test {

    private static final int ThreadNum = 10;

    public static void main(String[] args)  {

        //建立一個CountDownLatch例項,管理計數為ThreadNum
        CountDownLatch countDownLatch = new CountDownLatch(ThreadNum);

        //建立一個固定大小的執行緒池
        ExecutorService executor = Executors.newFixedThreadPool(ThreadNum);

        //新增執行緒到執行緒池
        for(int i =0;i<ThreadNum;++i){
            executor.execute(new Person(countDownLatch, i+1));
        }

        System.out.println("開始等待全員簽到...");

        try {
            //等待所有執行緒執行完畢
            countDownLatch.await();
            System.out.println("簽到完畢,開始吃飯");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

    }

    static class Person implements Runnable{

        private CountDownLatch countDownLatch;
        private int index;

        public Person(CountDownLatch cdl,int index){
            this.countDownLatch = cdl;
            this.index = index;
        }

        @Override
        public void run() {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("person " + index +"簽到");

            //執行緒執行完畢,計數器減一
            countDownLatch.countDown();

        }

    }
}複製程式碼

如上程式碼,建立一個執行緒池和CountDownLatch例項,每個執行緒通過建構函式傳入CountDownLatch的例項,主執行緒通過await等待執行緒池裡面執行緒任務全部執行完畢,子執行緒則執行完畢後呼叫countDown計數器減一,等所有子執行緒執行完畢後,主執行緒的await才會返回。

14.2 原理

先看下類圖:

可知CountDownLatch內部還是使用AQS實現的。首先通過建構函式初始化AQS的狀態值

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
    setState(count);
}複製程式碼

然後看下await方法:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果執行緒被中斷則拋異常
        if (Thread.interrupted())
            throw new InterruptedException();
        //嘗試看當前是否計數值為0,為0則直接返回,否者進入佇列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }複製程式碼

如果tryAcquireShared返回-1則 進入doAcquireSharedInterruptibly

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
       //加入佇列狀態為共享節點
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                       //如果多個執行緒呼叫了await被放入佇列則一個個返回。
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //shouldParkAfterFailedAcquire會把當前節點狀態變為SIGNAL型別,然後呼叫park方法把當先執行緒掛起,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }複製程式碼

呼叫await後,當前執行緒會被阻塞,直到所有子執行緒呼叫了countdown方法,並在計數為0時候呼叫該執行緒unpark方法啟用執行緒,然後該執行緒重新tryAcquireShared會返回1。

然後看下 countDown方法:

委託給sync
public void countDown() {
    sync.releaseShared(1);
}複製程式碼
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }複製程式碼

首先看下tryReleaseShared

protected boolean tryReleaseShared(int releases) {
    //迴圈進行cas,直到當前執行緒成功完成cas使計數值(狀態值state)減一更新到state
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}複製程式碼

該函式一直返回false直到當前計數器為0時候才返回true。返回true後會呼叫doReleaseShared,該函式主要作用是呼叫uppark方法啟用呼叫await的執行緒,程式碼如下:

private void doReleaseShared() {

    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //節點型別為SIGNAL,把型別在通過cas設定回去,然後呼叫unpark啟用呼叫await的執行緒
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}複製程式碼

啟用主執行緒後,主執行緒會在呼叫tryAcquireShared獲取鎖。

十五、ReentrantLock獨佔鎖原理

15.1 ReentrantLock結構

可知ReentrantLock最終還是使用AQS來實現,並且根據引數決定內部是公平還是非公平鎖,預設是非公平鎖

 public ReentrantLock() {
        sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}複製程式碼

加鎖程式碼:

public void lock() {
        sync.lock();
}複製程式碼

15.2 公平鎖原理

lock方法最終呼叫FairSync重寫的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
            //獲取當前執行緒和狀態值
            final Thread current = Thread.currentThread();
            int c = getState();
           //狀態為0說明該鎖未被任何執行緒持有
            if (c == 0) {
             //為了實現公平,首先看佇列裡面是否有節點,有的話再看節點所屬執行緒是不是當前執行緒,是的話hasQueuedPredecessors返回false,然後使用原子操作compareAndSetState保證一個執行緒更新狀態為1,設定排他鎖歸屬為當前執行緒。其他執行緒通過cass則返回false.
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
//狀態不為0說明該鎖已經被執行緒持有,則看是否是當前執行緒持有,是則重入鎖次數+1.
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");

                setState(nextc);
                return true;
            }
            return false;
        }
    }複製程式碼

公平性保證程式碼:

    public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }複製程式碼

再看看unLock方法,最終呼叫了Sync的tryRelease方法:

protected final boolean tryRelease(int releases) {
           //如果不是鎖持有者呼叫UNlock則丟擲異常。
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
           //如果當前可重入次數為0,則清空鎖持有執行緒
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //設定可重入次數為原始值-1
            setState(c);
            return free;
        }複製程式碼

15.3 非公平鎖原理

final void lock() {

   //如果當前鎖空閒0,則設定狀態為1,並且設定當前執行緒為鎖持有者
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);//呼叫重寫的tryAcquire方法->nonfairTryAcquire方法
}
複製程式碼

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//狀態為0說明沒有執行緒持有該鎖
                if (compareAndSetState(0, acquires)) {//cas原子性操作,保證只有一個執行緒可以設定狀態
                    setExclusiveOwnerThread(current);//設定鎖所有者
                    return true;
                }
            }//如果當前執行緒是鎖持有者則可重入鎖計數+1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }複製程式碼

15.3 總結

可知公平與非公平都是先執行tryAcquire嘗試獲取鎖,如果成功則直接獲取鎖,如果不成功則把當前執行緒放入佇列。對於放入佇列裡面的第一個執行緒A在unpark後會進行自旋呼叫tryAcquire嘗試獲取鎖,假如這時候有一個執行緒B執行了lock操作,那麼也會呼叫tryAcquire方法嘗試獲取鎖,但是執行緒B並不在佇列裡面,但是執行緒B有可能比執行緒A優先獲取到鎖,也就是說雖然執行緒A先請求的鎖,但是卻有可能沒有B先獲取鎖,這是非公平鎖實現。而公平鎖要保證執行緒A要比執行緒B先獲取鎖。所以公平鎖相比非公平鎖在tryAcquire裡面新增了hasQueuedPredecessors方法用來保證公平性。

十六、ReentrantReadWriteLock原理

如圖讀寫鎖內部維護了一個ReadLock和WriteLock,並且也提供了公平和非公平的實現,下面只介紹下非公平的讀寫鎖實現。我們知道AQS裡面只維護了一個state狀態,而ReentrantReadWriteLock則需要維護讀狀態和寫狀態,一個state是無法表示寫和讀狀態的。所以ReentrantReadWriteLock使用state的高16位表示讀狀態也就是讀執行緒的個數,低16位表示寫鎖可重入量。

static final int SHARED_SHIFT   = 16;

共享鎖(讀鎖)狀態單位值65536 
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
共享鎖執行緒最大個數65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

排它鎖(寫鎖)掩碼 二進位制 15個1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** 返回讀鎖執行緒數  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** 返回寫鎖可重入個數 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }複製程式碼

16.1 WriteLock

lock 獲取鎖


protected final boolean tryAcquire(int acquires) {

    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    //c!=0說明讀鎖或者寫鎖已經被某執行緒獲取
    if (c != 0) {
        //w=0說明已經有執行緒獲取了讀鎖或者w!=0並且當前執行緒不是寫鎖擁有者,則返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
       //說明某執行緒獲取了寫鎖,判斷可重入個數
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");

       // 設定可重入數量(1)
        setState(c + acquires);
        return true;
    }

   //寫執行緒獲取寫鎖
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
    複製程式碼

unlock 釋放鎖:

protected final boolean tryRelease(int releases) {
// 看是否是寫鎖擁有者呼叫的unlock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
//獲取可重入值,這裡沒有考慮高16位,因為寫鎖時候讀鎖狀態值肯定為0
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
//如果寫鎖可重入值為0則釋放鎖,否者只是簡單更新狀態值。
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}複製程式碼

16.2 ReadLock

對應讀鎖只需要分析下Sync的tryAcquireShared和tryReleaseShared

lock 獲取鎖:

protected final int tryAcquireShared(int unused) {

 //獲取當前狀態值
  Thread current = Thread.currentThread();
  int c = getState();

  //如果寫鎖計數不為0說明已經有執行緒獲取了寫鎖,然後看是不是當前執行緒獲取的寫鎖。
  if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
      return -1;

  //獲取讀鎖計數
  int r = sharedCount(c);
  //嘗試獲取鎖,多個讀執行緒只有一個會成功,不成功的進入下面fullTryAcquireShared進行重試
  if (!readerShouldBlock() &&
      r < MAX_COUNT &&
      compareAndSetState(c, c + SHARED_UNIT)) {
      if (r == 0) {
          firstReader = current;
          firstReaderHoldCount = 1;
      } else if (firstReader == current) {
          firstReaderHoldCount++;
      } else {
          HoldCounter rh = cachedHoldCounter;
          if (rh == null || rh.tid != current.getId())
              cachedHoldCounter = rh = readHolds.get();
          else if (rh.count == 0)
              readHolds.set(rh);
          rh.count++;
      }
      return 1;
  }
  return fullTryAcquireShared(current);
}複製程式碼

unlock 釋放鎖:

protected final boolean tryReleaseShared(int unused) {
  Thread current = Thread.currentThread();
  if (firstReader == current) {
      // assert firstReaderHoldCount > 0;
      if (firstReaderHoldCount == 1)
          firstReader = null;
      else
          firstReaderHoldCount--;
  } else {
      HoldCounter rh = cachedHoldCounter;
      if (rh == null || rh.tid != current.getId())
          rh = readHolds.get();
      int count = rh.count;
      if (count <= 1) {
          readHolds.remove();
          if (count <= 0)
              throw unmatchedUnlockException();
      }
      --rh.count;
  }

  //迴圈直到自己的讀計數-1 cas更新成功
  for (;;) {
      int c = getState();
      int nextc = c - SHARED_UNIT;
      if (compareAndSetState(c, nextc))

          return nextc == 0;
  }
}複製程式碼

十七、 什麼是重排序問題

Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序可以保證最終執行的結果是與程式順序執行的結果一致,並且只會對不存在資料依賴性的指令進行重排序,這個重排序在單執行緒下對最終執行結果是沒有影響的,但是在多執行緒下就會存在問題。

一個例子

int a = 1;(1)
int b = 2;(2)
int c= a + b;(3)複製程式碼

如上c的值依賴a和b的值,所以重排序後能夠保證(3)的操作在(2)(1)之後,但是(1)(2)誰先執行就不一定了,這在單執行緒下不會存在問題,因為並不影響最終結果。

一個多執行緒例子

public static class ReadThread extends Thread {
        public void run() {

            while(!Thread.currentThread().isInterrupted()){
                if(ready){(1)
                    System.out.println(num+num);(2)
                }
                System.out.println("read thread....");
            }

        }
    }

    public static class Writethread extends Thread {
        public void run() {
             num = 2;(3)
             ready = true;(4)
             System.out.println("writeThread set over...");
        }
    }

    private static int num =0;
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {

        ReadThread rt = new ReadThread();
        rt.start();

        Writethread  wt = new Writethread();
        wt.start();

        Thread.sleep(10);
        rt.interrupt();
        System.out.println("main exit");
    }複製程式碼

如程式碼由於(1)(2)(3)(4) 之間不存在依賴,所以寫執行緒(3)(4)可能被重排序為先執行(4)在執行(3),那麼執行(4)後,讀執行緒可能已經執行了(1)操作,並且在(3)執行前開始執行(2)操作,這時候列印結果為0而不是4.

解決:使用volatile 修飾ready可以避免重排序。

十八、 什麼是中斷

Java中斷機制是一種執行緒間協作模式,通過中斷並不能直接終止另一個執行緒,而是需要被中斷的執行緒根據中斷狀態自行處理。

例如當執行緒A執行時,執行緒B可以呼叫A的 interrupt()方法來設定中斷標誌為true,並立即返回。設定標誌僅僅是設定標誌,執行緒A並沒有實際被中斷,會繼續往下執行的,然後執行緒A可以呼叫isInterrupted方法來看自己是不是被中斷了,返回true說明自己被別的執行緒中斷了,然後根據狀態來決定是否終止自己活或者幹些其他事情。

Interrupted經典使用程式碼

public void run(){    
    try{    
         ....    
         //執行緒退出條件
         while(!Thread.currentThread().isInterrupted()&& more work to do){    
                // do more work;    
         }    
    }catch(InterruptedException e){    
                // thread was interrupted during sleep or wait    
    }    
    finally{    
               // cleanup, if required    
    }    
}複製程式碼

使用場景

故意呼叫interrupt()設定中斷標誌,作為執行緒退出條件:

  public static class MyThread extends Thread {
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {

                System.out.println("do Someing....");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        MyThread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }複製程式碼

當執行緒中為了等待一些特定條件的到來時候,一般會呼叫Thread.sleep(),wait,join方法在阻塞當前執行緒,比如sleep(3000);那麼到3s後才會從阻塞下變為啟用狀態,但是有可能在在3s內條件已經滿足了,這時候可以呼叫該執行緒的interrupt方法,sleep方法會丟擲InterruptedException異常,執行緒恢復啟用狀態:

 public static class SleepInterrupt extends Object implements Runnable{  
        public void run(){  
            try{  
                System.out.println("thread-sleep for 2000 seconds"); 

                Thread.sleep(2000000);  
                System.out.println("thread -waked up");  
            }catch(InterruptedException e){  
                System.out.println("thread-interrupted while sleeping");  

                return;    
            }  
            System.out.println("thread-leaving normally");  
        }  
    }

    public static void main(String[] args) throws InterruptedException {

        SleepInterrupt si = new SleepInterrupt();  
        Thread t = new Thread(si);  
        t.start();  

        //主執行緒休眠2秒,從而確保剛才啟動的執行緒有機會執行一段時間  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("main() - interrupting other thread");  
        //中斷執行緒t  
        t.interrupt();  

        System.out.println("main() - leaving");  

    }複製程式碼

InterruptedException的處理如果丟擲 InterruptedException那麼就意味著丟擲異常的方法是阻塞方法,比如Thread.sleep,wait,join。那麼接受到異常後如何處理的,醉簡單的是直接catch掉,不做任何處理,但是中斷髮生一般是為了取消任務或者退出執行緒來使用的,所以如果直接catch掉那麼就會失去做這些處理的時機,出發你能確定不需要根據中斷條件做其他事情。

  • 第一種方式 catch後做一些清理工作,然後在throw出去
  • 第二種方式 catch後,重新設定中斷標示

十九、FutureTask 原理

19.1 一個例子

static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子執行緒在進行計算");
            Thread.sleep(1000);
            int sum = 0;
            for (int i = 0; i < 100; i++)
                sum += i;
            return sum;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);

        System.out.println("主執行緒在執行任務");

        try {
            System.out.println("task執行結果" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("所有任務執行完畢");
        executor.shutdown();

    }複製程式碼

如上程式碼主執行緒會在futureTask.get()出阻塞直到task任務執行完畢,並且會返回結果。

19.2原理

FutureTask 內部有一個state用來展示任務的狀態,並且是volatile修飾的:

/** Possible state transitions:
 * NEW -> COMPLETING -> NORMAL 正常的狀態轉移
 * NEW -> COMPLETING -> EXCEPTIONAL 異常
 * NEW -> CANCELLED 取消
 * NEW -> INTERRUPTING -> INTERRUPTED 中斷
 */

private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;複製程式碼

其中構造FutureTask例項時候狀態為new

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;      
}複製程式碼

把FutureTask提交到執行緒池或者執行緒執行start時候會呼叫run方法:

public void run() {

    //如果當前不是new狀態,或者當前cas設定當前執行緒失敗則返回,只有一個執行緒可以成功。
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        //當前狀態為new 則呼叫任務的call方法執行任務
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);完成NEW -> COMPLETING -> EXCEPTIONAL 狀態轉移
            }

            //執行任務成功則儲存結果更新狀態,unpark所有等待執行緒。
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

protected void set(V v) {
    //狀態從new->COMPLETING
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        //狀態從COMPLETING-》NORMAL
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        //unpark所有等待執行緒。
        finishCompletion();
    }
}複製程式碼

任務提交後,會呼叫 get方法獲取結果,這個get方法是阻塞的。

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //如果當前狀態是new或者COMPLETING則等待,因為位normal或者exceptional時候才說明資料計算完成了。
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
}複製程式碼

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {

        //如果被中斷,則拋異常
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        //組建單列表
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {

            nanos = deadline - System.nanoTime();
            //超時則返回
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            //否者設定park超時時間
            LockSupport.parkNanos(this, nanos);
        }
        else
            //直接掛起當前執行緒
            LockSupport.park(this);
    }
}複製程式碼

private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
}複製程式碼

在submit任務後還可以呼叫futuretask的cancel來取消任務:

public boolean cancel(boolean mayInterruptIfRunning) {
        //只有任務是new的才能取消
        if (state != NEW)
            return false;
       //執行時允許中斷
        if (mayInterruptIfRunning) {
           //完成new->INTERRUPTING
            if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, INTERRUPTING))
                return false;
            Thread t = runner;
            if (t != null)
                t.interrupt();
            //完成INTERRUPTING->INTERRUPTED
            UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); // final state
        }
       //不允許中斷則直接new->CANCELLED
        else if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED))
            return false;
        finishCompletion();
        return true;
    }複製程式碼

二十、ConcurrentHashMap原理簡述

翻看ConcurrentHashMap的原始碼知道ConcurrentHashMap使用分離鎖,整個map分段segment,每個segments是繼承了ReentrantLock,使用ReentrantLock的獨佔鎖用來控制同一個段只能有一個執行緒進行寫,但是不同段可以多個執行緒同時寫。另外無論是段內還是段外多個執行緒都可以同時讀取,因為他使用了volatile語義的讀,並沒加鎖。並且當前段有寫執行緒時候,該段也允許多個讀執行緒存在。

put的大概邏輯,首先計算key的hash值,然後根據一定演算法(位移和與操作)計算出該元素應該放到那個segment,然後呼叫segment.put方法,該方法裡面使用ReentrantLock進行寫控制,第一個執行緒tryLock獲取鎖進行寫入,其他寫執行緒則自旋呼叫tryLock 迴圈嘗試。

get的大概邏輯,使用UNSAFE.getObjectVolatile 在不加鎖情況下獲取volatile語義的值。

關注公眾號《架構文摘》,每天一篇架構領域重磅好文,涉及一線網際網路公司應用架構(高可用、高效能、高穩定)、大資料、機器學習、Java架構等各個熱門領域。

相關文章