詳解AQS的7個同步元件

華為雲開發者聯盟發表於2023-03-13
摘要:AQS的全稱為Abstract Queued Synchronizer,是在J.U.C(java.util.concurrent)下子包中的類。

本文分享自華為雲社群《【高併發】AQS案例詳解》,作者: 冰 河。

AQS的全稱為Abstract Queued Synchronizer,是在J.U.C(java.util.concurrent)下子包中的類。

一、AQS的設計如下

(1)使用Node實現FIFO佇列,可以用於構建鎖或者其他同步裝置的基礎框架。

(2)利用了一個int型別表示狀態

在AQS類中,有一個叫做state的成員變數。

基於AQS有一個同步元件ReentrantLock,在ReentrantLock中,state表示獲取鎖的執行緒數。如果state=0,則表示還沒有執行緒獲取鎖;如果state=1,則表示有執行緒獲取了鎖;如果state>1,則表示重入鎖的數量。

(3)使用方法是繼承

設計上基於模板方法,使用時需要繼承AQS,並覆寫其中的方法

(4)子類透過繼承並透過實現它的方法管理其狀態{acquire和release}的方法操縱狀態

(5)可以同時實現排它鎖和共享鎖模式(獨佔、共享)

站在使用者的角度,AQS的功能主要分為兩類:獨佔模式和共享模式。它的所有子類中要麼實現並使用了它的獨佔功能的API,要麼使用了共享鎖的功能,而不會同時使用兩套API。即便是它最有名的子類——ReentrantReadWriteLock,也是透過兩個內部類——ReadLock(讀鎖)和WriteLock(寫鎖)兩套API來實現的。

二、AQS內部實現的大體思路

首先,AQS內部維護了一個CLH佇列來管理鎖,執行緒會首先嚐試獲取鎖,如果失敗,就將當前執行緒以及等待等資訊封裝成一個Node節點,加入到同步佇列SyncQueue,接著會不斷迴圈嘗試獲取鎖,獲取鎖的條件是當前節點為Head的直接後繼節點才會嘗試獲取鎖,如果失敗,就會阻塞自己,直到自己被喚醒。而持有鎖的執行緒釋放鎖的時候,會喚醒佇列中的後繼執行緒。基於這些基礎的設計和思路,JDK提供了許多基於AQS的子類,比如:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition、FutureTask等

三、AQS同步元件

  • CountDownLatch:閉鎖,透過一個計數,來保證執行緒是否一直阻塞
  • Semaphore:控制同一時間併發執行緒的數目
  • CyclicBarrier:與CountDownLatch類似,都能阻塞程式;
  • ReentrantLock:可重入鎖
  • Condition: 在使用時需要ReentrantLock
  • FutureTask:對比Runnable和Callable

1.CountDownLatch

同步輔助類,透過它可以阻塞當前執行緒。也就是說,能夠實現一個執行緒或者多個執行緒一直等待,直到其他執行緒執行的操作完成。使用一個給定的計數器進行初始化,該計數器的操作是原子操作,即同時只能有一個執行緒操作該計數器。
呼叫該類await()方法的執行緒會一直阻塞,直到其他執行緒呼叫該類的countDown()方法,使當前計數器的值變為0為止。每次呼叫該類的countDown()方法,當前計數器的值就會減1。當計數器的值減為0的時候,所有因呼叫await()方法而處於等待狀態的執行緒就會繼續往下執行。這種操作只能出現一次,因為該類中的計數器不能被重置。如果需要一個可以重置計數次數的版本,可以考慮使用CyclicBarrier類。
CountDownLatch支援給定時間的等待,超過一定的時間不再等待,使用時只需要在await()方法中傳入需要等待的時間即可。此時,await()方法的方法簽名如下:

public boolean await(long timeout, TimeUnit unit)

CountDownLatch使用場景

在某些業務場景中,程式執行需要等待某個條件完成後才能繼續執行後續的操作。典型的應用為平行計算:當某個處理的運算量很大時,可以將該運算任務拆分成多個子任務,等待所有的子任務都完成之後,父任務再拿到所有子任務的運算結果進行彙總。

呼叫ExecutorService類的shutdown()方法,並不會第一時間內把所有執行緒全部都銷燬掉,而是讓當前已有的執行緒全部執行完,之後,再把執行緒池銷燬掉。

示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CountDownLatchExample {
 private static final int threadCount = 200;
 public static void main(String[] args) throws InterruptedException {
 ExecutorService exec = Executors.newCachedThreadPool();
 final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
 for (int i = 0; i < threadCount; i++){
 final int threadNum = i;
 exec.execute(() -> {
 try {
 test(threadNum);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }finally {
 countDownLatch.countDown();
 }
 });
 }
 countDownLatch.await();
        log.info("finish");
 exec.shutdown();
 }
 private static void test(int threadNum) throws InterruptedException {
 Thread.sleep(100);
 log.info("{}", threadNum);
 Thread.sleep(100);
 }
}

支援給定時間等待的示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CountDownLatchExample {
 private static final int threadCount = 200;
 public static void main(String[] args) throws InterruptedException {
 ExecutorService exec = Executors.newCachedThreadPool();
 final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
 for (int i = 0; i < threadCount; i++){
 final int threadNum = i;
 exec.execute(() -> {
 try {
 test(threadNum);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }finally {
 countDownLatch.countDown();
 }
 });
 }
 countDownLatch.await(10, TimeUnit.MICROSECONDS);
        log.info("finish");
 exec.shutdown();
 }
 private static void test(int threadNum) throws InterruptedException {
 Thread.sleep(100);
 log.info("{}", threadNum);
 }
}

2.Semaphore

控制同一時間併發執行緒的數目。能夠完成對於訊號量的控制,可以控制某個資源可被同時訪問的個數。

提供了兩個核心方法——acquire()方法和release()方法。acquire()方法表示獲取一個許可,如果沒有則等待,release()方法則是在操作完成後釋放對應的許可。Semaphore維護了當前訪問的個數,透過提供同步機制來控制同時訪問的個數。Semaphore可以實現有限大小的連結串列。

Semaphore使用場景如

Semaphore常用於僅能提供有限訪問的資源,比如:資料庫連線數

每次獲取並釋放一個許可,示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SemaphoreExample {
 private static final int threadCount = 200;
 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) {
 e.printStackTrace();
 }
 });
 }
 exec.shutdown();
 }
 private static void test(int threadNum) throws InterruptedException {
 log.info("{}", threadNum);
 Thread.sleep(1000);
 }
}

每次獲取並釋放多個許可,示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SemaphoreExample {
 private static final int threadCount = 200;
 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(3); //獲取多個許可
 test(threadNum);
 semaphore.release(3); //釋放多個許可
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
 }
        log.info("finish");
 exec.shutdown();
 }
 private static void test(int threadNum) throws InterruptedException {
 log.info("{}", threadNum);
 Thread.sleep(1000);
 }
}

假設有這樣一個場景,併發太高了,即使使用Semaphore進行控制,處理起來也比較棘手。假設系統當前允許的最高併發數是3,超過3後就需要丟棄,使用Semaphore也能實現這樣的場景,示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SemaphoreExample {
 private static final int threadCount = 200;
 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的原始碼
 if (semaphore.tryAcquire()) { 
 test(threadNum);
 semaphore.release(); //釋放一個許可
 }
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
 }
        log.info("finish");
 exec.shutdown();
 }
 private static void test(int threadNum) throws InterruptedException {
 log.info("{}", threadNum);
 Thread.sleep(1000);
 }
}

3.CyclicBarrier

是一個同步輔助類,允許一組執行緒相互等待,直到到達某個公共的屏障點,透過它可以完成多個執行緒之間相互等待,只有當每個執行緒都準備就緒後,才能各自繼續往下執行後面的操作。

與CountDownLatch有相似的地方,都是使用計數器實現,當某個執行緒呼叫了CyclicBarrier的await()方法後,該執行緒就進入了等待狀態,而且計數器執行加1操作,當計數器的值達到了設定的初始值,呼叫await()方法進入等待狀態的執行緒會被喚醒,繼續執行各自後續的操作。CyclicBarrier在釋放等待執行緒後可以重用,所以,CyclicBarrier又被稱為迴圈屏障。

CyclicBarrier使用場景

可以用於多執行緒計算資料,最後合併計算結果的場景

CyclicBarrier與CountDownLatch的區別

(1)CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法進行重置,並且可以迴圈使用
(2)CountDownLatch主要實現1個或n個執行緒需要等待其他執行緒完成某項操作之後,才能繼續往下執行,描述的是1個或n個執行緒等待其他執行緒的關係。而CyclicBarrier主要實現了多個執行緒之間相互等待,直到所有的執行緒都滿足了條件之後,才能繼續執行後續的操作,描述的是各個執行緒內部相互等待的關係。
(3)CyclicBarrier能夠處理更復雜的場景,如果計算發生錯誤,可以重置計數器讓執行緒重新執行一次。
CyclicBarrier中提供了很多有用的方法,比如:可以透過getNumberWaiting()方法獲取阻塞的執行緒數量,透過isBroken()方法判斷阻塞的執行緒是否被中斷。

示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CyclicBarrierExample {
 private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
 public static void main(String[] args) throws Exception {
 ExecutorService executorService = Executors.newCachedThreadPool();
 for (int i = 0; i < 10; i++){
 final int threadNum = i;
 Thread.sleep(1000);
 executorService.execute(() -> {
 try {
 race(threadNum);
 } catch (Exception e) {
 e.printStackTrace();
 }
 });
 }
executorService.shutdown();
 }
 private static void race(int threadNum) throws Exception{
 Thread.sleep(1000);
 log.info("{} is ready", threadNum);
 cyclicBarrier.await();
 log.info("{} continue", threadNum);
 }
}

設定等待超時示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class CyclicBarrierExample {
 private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
 public static void main(String[] args) throws Exception {
 ExecutorService executorService = Executors.newCachedThreadPool();
 for (int i = 0; i < 10; i++){
 final int threadNum = i;
 Thread.sleep(1000);
 executorService.execute(() -> {
 try {
 race(threadNum);
 } catch (Exception e) {
 e.printStackTrace();
 }
 });
 }
 executorService.shutdown();
 }
 private static void race(int threadNum) throws Exception{
 Thread.sleep(1000);
 log.info("{} is ready", threadNum);
 try{
 cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
 }catch (BrokenBarrierException | TimeoutException e){
 log.warn("BarrierException", e);
 }
 log.info("{} continue", threadNum);
 }
}

在宣告CyclicBarrier的時候,還可以指定一個Runnable,當執行緒達到屏障的時候,可以優先執行Runnable中的方法。

示例程式碼如下:

package io.binghe.concurrency.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CyclicBarrierExample {
 private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
 log.info("callback is running");
 });
 public static void main(String[] args) throws Exception {
 ExecutorService executorService = Executors.newCachedThreadPool();
 for (int i = 0; i < 10; i++){
 final int threadNum = i;
 Thread.sleep(1000);
 executorService.execute(() -> {
 try {
 race(threadNum);
 } catch (Exception e) {
 e.printStackTrace();
 }
 });
 }
 executorService.shutdown();
 }
 private static void race(int threadNum) throws Exception{
 Thread.sleep(1000);
 log.info("{} is ready", threadNum);
 cyclicBarrier.await();
 log.info("{} continue", threadNum);
 }
}

4.ReentrantLock與鎖

Java中主要分為兩類鎖,一類是synchronized修飾的鎖,另外一類就是J.U.C中提供的鎖。J.U.C中提供的核心鎖就是ReentrantLock。

ReentrantLock(可重入鎖)與synchronized區別:

(1)可重入性
二者都是同一個執行緒進入1次,鎖的計數器就自增1,需要等到鎖的計數器下降為0時,才能釋放鎖。
(2)鎖的實現
synchronized是基於JVM實現的,而ReentrantLock是JDK實現的
(3)效能的區別
synchronized最佳化之前效能比ReentrantLock差很多,但是自從synchronized引入了偏向鎖,輕量級鎖也就是自旋鎖後,效能就差不多了。
(4)功能區別

  • 便利性:synchronized使用起來比較方便,並且由編譯器保證加鎖和釋放鎖;ReentrantLock需要手工宣告加鎖和釋放鎖,最好是在finally程式碼塊中宣告釋放鎖。
  • 鎖的靈活度和細粒度:在這點上ReentrantLock會優於synchronized

ReentrantLock獨有的功能如下:

(1)ReentrantLock可指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。
(2)提供了一個Condition類,可以分組喚醒需要喚醒的執行緒。而synchronized只能隨機喚醒一個執行緒,或者喚醒全部的執行緒
(3)提供能夠中斷等待鎖的執行緒的機制,lock.lockInterruptibly()。ReentrantLock實現是一種自旋鎖,透過迴圈呼叫CAS操作來實現加鎖,效能上比較好是因為避免了使執行緒進入核心態的阻塞狀態。

synchronized能做的事情ReentrantLock都能做,而ReentrantLock有些能做的事情,synchronized不能做。
在效能上,ReentrantLock不會比synchronized差。

synchronized的優勢:

(1)不用手動釋放鎖,JVM自動處理,如果出現異常,JVM也會自動釋放鎖
(2)JVM用synchronized進行管理鎖定請求和釋放時,JVM在生成執行緒轉儲時能夠鎖定資訊,這些對除錯非常有價值,因為它們能標識死鎖或者其他異常行為的來源。而ReentrantLock只是普通的類,JVM不知道具體哪個執行緒擁有lock物件。
(3)synchronized可以在所有JVM版本中工作,ReentrantLock在某些1.5之前版本的JVM中可能不支援

ReentrantLock中的部分方法說明:

  • boolean tryLock():僅在呼叫時鎖定未被另一個執行緒保持的情況下才獲取鎖定
  • boolean tryLock(long, TimeUnit): 如果鎖定在給定的等待時間內沒有被另一個執行緒保持,且當前執行緒沒有被中斷,則獲取這個鎖定。
  • void lockInterruptibly():如果當前執行緒沒有被中斷,就獲取鎖定;如果被中斷,就丟擲異常
  • boolean isLocked():查詢此鎖定是否由任意執行緒保持
  • boolean isHeldByCurrentThread(): 查詢當前執行緒是否保持鎖定狀態;
  • boolean isFair():判斷是否是公平鎖
  • boolean hasQueuedThread(Thread):查詢指定執行緒是否在等待獲取此鎖定
  • boolean hasQueuedThreads():查詢是否有執行緒正在等待獲取此鎖定
  • boolean getHoldCount():查詢當前執行緒保持鎖定的個數

示例程式碼如下:

package io.binghe.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample {
 //請求總數
 public static int clientTotal = 5000;
 //同時併發執行的執行緒數
 public static int threadTotal = 200;
 public static int count = 0;
 private static final Lock lock = new ReentrantLock();
 public static void main(String[] args) throws InterruptedException {
 ExecutorService executorService = Executors.newCachedThreadPool();
 final Semaphore semaphore = new Semaphore(threadTotal);
 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
 for(int i = 0; i < clientTotal; i++){
 executorService.execute(() -> {
 try{
 semaphore.acquire();
 add();
 semaphore.release();
 }catch (Exception e){
 log.error("exception", e);
 }
 countDownLatch.countDown();
 });
 }
 countDownLatch.await();
 executorService.shutdown();
        log.info("count:{}", count);
 }
 private static void add(){
 lock.lock();
 try{
            count ++;
 }finally {
 lock.unlock();
 }
 }
}

5.ReentrantReadWriteLock

在沒有任何讀寫鎖的時候,才可以取得寫鎖。如果一直有讀鎖存在,則無法執行寫鎖,這就會導致寫鎖飢餓。
示例程式碼如下:

package io.binghe.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class LockExample {
 private final Map<String, Data> map = new TreeMap<>();
 private final 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 {
 writeLock.unlock();
 }
 }
 class Data{
 }
}

6.StampedLock

控制鎖三種模式:寫、讀、樂觀讀。

StampedLock的狀態由版本和模式兩個部分組成,鎖獲取方法返回的是一個數字作為票據,用相應的鎖狀態來表示並控制相關的訪問,數字0表示沒有寫鎖被授權訪問。

在讀鎖上分為悲觀鎖和樂觀鎖,樂觀讀就是在讀操作很多,寫操作很少的情況下,可以樂觀的認為寫入和讀取同時發生的機率很小。因此,不悲觀的使用完全的讀取鎖定。程式可以檢視讀取資料之後,是否遭到寫入進行了變更,再採取後續的措施,這樣的改進可以大幅度提升程式的吞吐量。

總之,在讀執行緒越來越多的場景下,StampedLock大幅度提升了程式的吞吐量。

StampedLock原始碼中的案例如下,這裡加上了註釋

class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//下面看看樂觀讀鎖案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //獲得一個樂觀讀鎖
double currentX = x, currentY = y; //將兩個欄位讀入本地區域性變數
if (!sl.validate(stamp)) { //檢查發出樂觀讀鎖後同時是否有其他寫鎖發生?
stamp = sl.readLock(); //如果沒有,我們再次獲得一個讀悲觀鎖
try {
currentX = x; // 將兩個欄位讀入本地區域性變數
currentY = y; // 將兩個欄位讀入本地區域性變數
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//下面是悲觀讀鎖案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //迴圈,檢查當前狀態是否符合
long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉為寫鎖
if (ws != 0L) { //這是確認轉為寫鎖是否成功
stamp = ws; //如果成功 替換票據
x = newX; //進行狀態改變
y = newY; //進行狀態改變
break;
} else { //如果不能成功轉換為寫鎖
sl.unlockRead(stamp); //我們顯式釋放讀鎖
stamp = sl.writeLock(); //顯式直接進行寫鎖 然後再透過迴圈再試
}
}
} finally {
sl.unlock(stamp); //釋放讀鎖或寫鎖
}
}
}

示例程式碼如下:

package io.binghe.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.StampedLock;
@Slf4j
public class LockExample {
 //請求總數
 public static int clientTotal = 5000;
 //同時併發執行的執行緒數
 public static int threadTotal = 200;
 public static int count = 0;
 private static final StampedLock lock = new StampedLock();
 public static void main(String[] args) throws InterruptedException {
 ExecutorService executorService = Executors.newCachedThreadPool();
 final Semaphore semaphore = new Semaphore(threadTotal);
 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
 for(int i = 0; i < clientTotal; i++){
 executorService.execute(() -> {
 try{
 semaphore.acquire();
 add();
 semaphore.release();
 }catch (Exception e){
 log.error("exception", e);
 }
 countDownLatch.countDown();
 });
 }
 countDownLatch.await();
 executorService.shutdown();
        log.info("count:{}", count);
 }
 private static void add(){
//加鎖時返回一個long型別的票據
 long stamp = lock.writeLock();
 try{
            count ++;
 }finally {
 //釋放鎖的時候帶上加鎖時返回的票據
 lock.unlock(stamp);
 }
 }
}

總結:

(1)當只有少量競爭者時,synchronized是一個很好的通用鎖實現
(2)競爭者不少,但是執行緒的增長趨勢是可預估的,此時,ReentrantLock是一個很好的通用鎖實現
(3)synchronized不會引發死鎖,其他的鎖使用不當可能會引發死鎖。

7.Condition

Condition是一個多執行緒間協調通訊的工具類,Condition除了實現wait和notify的功能以外,它的好處在於一個lock可以建立多個Condition,可以選擇性的通知wait的執行緒

特點:

(1)Condition 的前提是Lock,由AQS中newCondition()方法 建立Condition的物件
(2)Condition await方法表示執行緒從AQS中移除,並釋放執行緒獲取的鎖,並進入Condition等待佇列中等待,等待被signal
(3)Condition signal方法表示喚醒對應Condition等待佇列中的執行緒節點,並加入AQS中,準備去獲取鎖。

示例程式碼如下:

package io.binghe.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample {
 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.await();
 } 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();
 }
}

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章