JUC包下的AQS --- 隊列同步器

JUC包下的AQS — 隊列同步器

AQS簡介

  AQS,即AbstractQueuedSynchronizer,在java.util.concurrent.locks包下面。AQS是用來構建鎖和同步器的框架,基於AQS可以簡單高效的開發出適合自己的同步器。
  ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的,AQS可以認爲是JUC的核心。

可重入鎖非可重入鎖

  在講解原理之前,我們先弄清楚兩個概念,可重入鎖非可重入鎖。可重入鎖指的是當一個線程獲取鎖之後,可以再次獲取該鎖,從而避免死鎖。
  ReentrantLock和synchronized都是可重入鎖,AQS中共享變量state的初始值爲0。對於可重入鎖,當一個線程嘗試獲取鎖時,會判斷state是否等於0,等於0則可以獲取鎖,並將state置爲1,若state值不是0,會判斷獲取鎖的線程是否是佔有鎖的線程,如果是則執行state++。釋放鎖時,同樣的也會執行state–,直到state等於0,線程纔會真正釋放鎖。
  對於非可重入鎖,當一個線程嘗試獲取鎖時,判斷state值是不是0,是0則可以獲取鎖,不是0則進入阻塞狀態,如果是同一個線程就會產生死鎖。

樂觀鎖和悲觀鎖

悲觀鎖
  總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。
  
樂觀鎖
  總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

這篇博客對於樂觀鎖和悲觀鎖寫的非常詳細。
面試必備之樂觀鎖與悲觀鎖

AQS原理

  1.使用Node實現FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架。
  2.使用一個int類型表示狀態。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性

  3.子類通過繼承並通過實現它的方法管理其狀態{acquire和release}的方法操縱狀態。
  4.可以同時實現排它鎖和共享鎖模式(獨佔、共享),指的是實現它的子類可以選擇獨佔或者共享,不是說可以選擇獨佔和共享。

Exclusive(獨佔): 只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖。   公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖。
  非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的。
Share(共享): 多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、CyclicBarrier、ReadWriteLock。

  AQS的核心思想是: 當共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
  並且只有head節點的直接後繼節點可以請求獲取鎖,失敗則將自己阻塞直到獲取成功。

AQS同步組件

CountDownLatch

  在程序執行過程中,可能需要滿足某些條件纔可以繼續執行後續的操作,所以JDK1.5之後引入了該類。

在這裏插入圖片描述

//調用該方法的線程會被暫時掛起,直到count值減到0,纔會被喚醒繼續執行。
public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}

//和await()方法用法相同,可以設置時間,當一段時間後沒有達到條件也會被繼續執行
public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

//令count值減一
public void countDown() {
        sync.releaseShared(1);
}

  CountDownLatch可以使一個線程等待其他線程各自執行完畢後再執行,但是不支持count值進行重置,如果業務上有要求的話,可以考慮使用CyclicBarrier。

Semaphore(信號量)

  可以控制某個資源可以被同時訪問的線程個數,下面這是兩個比較重要的方法。

//獲得一個許可
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}

//方法可以傳入permits參數,代表依次獲取多個,同樣可以依次釋放多個
public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
}

//釋放一個許可
public void release() {
        sync.releaseShared(1);
}

例子:

private static int threadCount = 20;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //獲取許可
                    semaphore.acquire();
                    test(threadNum);
                    //釋放許可
                    semaphore.release();
                } catch (InterruptedException e) {
                    log.info("exception",e);
                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
	}

  從這個例子可以看出,執行結果是3個3個出現的,對應了new Semaphore(3)將信號量設置爲3。

Semaphore還有一個比較重要的方法:tryAcquire()

//嘗試獲取信號量,在特定時間獲取不到則放棄執行
public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

CyclicBarrier

  CyclicBarrier和CountDownLatch用法很相似,只不過CountDownLatch執行的是減一操作,CyclicBarrier執行的是加一操作。可以實現一組線程相互等待,直到到達某個條件纔會繼續執行,當一個線程調用await()方法時,就會進入阻塞狀態,直到達到臨界值纔會喚醒所有等待的線程繼續執行。
  比如可以對多個目標進行相加的操作時,需要等待每個線程都操作完畢時再進行統一相加,就可以使用CyclicBarrier來實現。相對的,CyclicBarrier的臨界值可以循環使用。

在這裏插入圖片描述

//可以設置等待時間,超過時間就算沒有到達指定條件,依然會執行	
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
}

例子:

private static CyclicBarrier barrier = new CyclicBarrier(5);

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

        ExecutorService exec = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            exec.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.info("Exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        try {
            barrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (BrokenBarrierException  e) {
            log.info("BarrierException", e);
        }
        log.info("{} continue", threadNum);
    }

  需要注意的是: 設置時間可能會拋出BrokenBarrierException異常,如果希望下面的代碼繼續執行,就需要將異常捕獲而不讓其拋出。
  BrokenBarrierException這個異常的意思就是,當某個等待的線程被中斷,其它等待的線程會拋出這個異常。

//這樣做可以在達到條件時,先執行這裏的代碼
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        log.info("callback is running");
});

ReentrantLock(可重入鎖)

  ReentrantLock和synchronized的區別:

① 兩者都是可重入鎖
② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API
③ReenTrantLock 比 synchronized 增加了一些高級功能
  1.ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,synchronized 則不能。
  2.ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
  3.ReentrantLock類線程對象可以註冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現選擇性通知

在synchronized可以實現的情況下,還是比較推薦使用synchronized。

我們來看一下部分源碼
//無參構造方法,默認給了一個不公平鎖
public ReentrantLock() {
        sync = new NonfairSync();
}
//有參構造方法,可以自己設着公平或非公平
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

//嘗試獲取鎖,如果沒有被其它線程鎖定,則獲取鎖
//也可以傳入時間,代表一段時間後,鎖沒有被其它線程鎖定,則獲取鎖
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
}

例子:

//聲明一個鎖
private final static Lock lock = new ReentrantLock();

//切記解鎖一定要放在finally中,防止出現死鎖的情況
private static void add() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
}

ReentrantReadWriteLock(讀寫鎖)

  在沒有任何讀寫鎖的時候,纔可以獲得寫鎖,這實際上是用的悲觀鎖的設計思想。所以在讀操作頻繁,寫操作不頻繁的情況下,有可能會造成飢餓現象。比如,一個線程希望進行寫操作,但是由於寫操作很頻繁,寫進程就會一直等待。

用法:
	private final Map<String, Data> map = new TreeMap<>();

    private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys(){
        readLock.lock();
        try{
            return map.keySet();
        }finally {
            readLock.unlock();
        }
    }

    public Data put(String key,Data value){
        writeLock.lock();
        try{
            return map.put(key,value);
        }finally {
            readLock.unlock();
        }
    }

    class Data{

    }

Condition

  前面在介紹AQS時,我們知道AQS實際上是通過內置的FIFO隊列來完成獲取資源線程的排隊工作。
  實際上在這個隊列下面還存在着一個Condition隊列,暫時可以將其理解爲一個假死隊列,調用方法進入該隊列的線程就像是死掉了一樣,但是它是在等待一個時機,條件達到的時候可以調用方法將其喚醒繼續執行。
在這裏插入圖片描述

	public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(() ->{
            try{
                reentrantLock.lock();
                log.info("wait signal");//1
                condition.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            log.info("get signal");//4
            reentrantLock.unlock();
        }).start();

        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock");//2
            try{
                Thread.sleep(3000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal");//3
            reentrantLock.unlock();
        }).start();
    }

  代碼中的的註釋代表的是執行的順序,當線程執行condition. wait()之後,該線程就會進入Condition隊列,並且相當於執行了unlock()進行了解鎖。
  當另一個線程嘗試獲取該線程時可以獲取,當執行condition.signalAll()之後,就會喚醒Condition隊列中所有的線程,當然也可以一個一個的喚醒,之後繼續執行。
  實際上,用到Condition的場景很少,這裏當做瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章