1.AQS簡介
1.1概念:
AbstractQueuedSynchronizer,它提供了一個FIFO隊列,可以看做是一個可以用來實現鎖以及其他需要同步功能的框架。這裏簡稱該類爲AQS。AQS的使用依靠繼承來完成,子類通過繼承自AQS並實現所需的方法來管理同步狀態。例如ReentrantLock,CountDownLatch等.
1.2底層結構。
AQS底層使用的雙向鏈表實現,如下圖所示。
雙向鏈表是隊列的一個實現,因此我們也可以把他當成一個隊列,其中Sync queue是同步隊列,包含head節點和tail節點。head節點主要用於後續的調度;Condition queue 是一個單鏈表。這個結構不是必須的,當需要使用到整個結構的時候纔會生成,且可能會有多個。
1.3 AQS的設計
① 使用Node實現FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架;
② 利用了一個int類型來表示狀態(在AQS類中有一個state的成員變量);
③ 使用方法是繼承,AQS是基於模板方法使用者需要在使用的時候先繼承,並複寫其中的方法;
④ 子類通過繼承並通過實現它的方法來管理其狀態(通過acquire與release來操作狀態);
⑤ 可以同時實現排它鎖和共享鎖模式(獨佔與共享);
1.4 AQS實現的思路
首先AQS內部維護了一個CLH隊列(在AQS中CLH隊列是維護一組線程的嚴格按照FIFO的隊列。他能夠確保無飢餓,嚴格的先來先服務的公平性。)來管理鎖,線程會首先嚐試獲取鎖,如果失敗會將當前線程以及等待狀態等信息包裝成一個node加入到Sync queue 同步隊列中,接着會不斷的循環嘗試獲取鎖,它的條件是當前節點head的直接後繼纔會嘗試。如果失敗就會阻塞自己,直到自己被喚醒,而當持有鎖的線程釋放鎖的時候會喚醒隊列中的後繼線程,基於這些基礎的設計和思路,JDK提供了需要基於AQS的子類。
1.5 AQS常用組件
① CountDownLatch : 是一個閉鎖,通過一個計數來保證線程是否需要一直阻塞;
② Semaphore : 它能控制同一時間併發線程的數目;
③ CyclicBarrier : 它和CountDownLatch相似都能阻塞進程
④ ReentrantLock : 可重入互斥鎖Lock
⑤ Condition : 提供了類似的Object的監視器方法,與Lock配合可以實現等待/通知模式.
⑥ FutureTask : 可用於異步獲取執行結果或取消執行任務的場景。
2.CountDownLatch
2.1CountDownLatch簡介:
CountDownLatch是一個同步輔助類,通過它可以完成類似於阻塞當前線程的功能(換句話說就是一個線程或者多個線程一直等待直到其他線程操作完成),CountDownLatch用了一個給定的計數器來進行初始化,該計數器的操作是原子操作(同時只能有一個線程去操作該計數器),調用該類await()方法的線程會一直處於阻塞狀態,直到其他線程調用countDown()這個方法使計數器的值變成0,每次調用countDown()的時候計數器會減1,當計數器的值變成0的時候,所有因調用await()方法而處於等待狀態的線程就會繼續往下執行,這種操作只會出現一次因爲計數器不能被重置,如果大家在實際開發過程需要一個可以重置計數器的版本可以考慮使用CyclicBarrier來實現;
2.2 CountDownLatch的使用場景
在某些業務場景中,程序執行需要等待某個條件完成後才能繼續執行後續的操作,典型的應用比如 : 並行計算
2.3 代碼演示:
// CountDownLatch 1
@Slf4j
public class CountDownLatchExample1 {
private final static int threadCount = 200;
public static void main(String[] args) throws Exception {
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 (Exception e) {
log.error("exception", e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("{}", threadNum);
Thread.sleep(100);
}
}
// CountDownLatch 2
@Slf4j
public class CountDownLatchExample2 {
private final static int threadCount = 200;
public static void main(String[] args) throws Exception {
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 (Exception e) {
log.error("exception", e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.MILLISECONDS);
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("{}", threadNum);
}
}
3.Semaphore
3.1Semaphore簡介
Semaphore又稱信號量,它可以控制併發訪問的線程個數,在操作系統中信號量是一個非常重要的概念,在進程控制方面有很重要的應用,java併發庫中的Semaphore可以非常輕鬆的完成類似於操作系統中信號量的控制,Semaphore可以控制某個資源可被同時訪問的個數,與CountDownLatch的使用有一些類似也是提供了兩個核心的方法(acquire()與release()),acquire方法是獲取一個許可如果沒有則會等待,release是在操作完成後釋放一個許可出來,Semaphore維護了當前訪問的個數通過提供同步機制來控制同時訪問的個數,Semaphore可以實現有限大小的鏈表;
3.2Semaphore使用場景
Semaphore常用與僅能提供有限訪問的資源,比如項目中使用的數據庫(數據庫所支持的最大連接數)
3.3代碼演示
// Semaphore 1
@Slf4j
public class SemaphoreExample1 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
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 (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
// Semaphore 2
@Slf4j
public class SemaphoreExample2 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
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 (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
// Semaphore 3
@Slf4j
public class SemaphoreExample3 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
if (semaphore.tryAcquire()) { // 嘗試獲取一個許可
test(threadNum);
semaphore.release(); // 釋放一個許可
}
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
// Semaphore 4
@Slf4j
public class SemaphoreExample4 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
if (semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { // 嘗試獲取一個許可
test(threadNum);
semaphore.release(); // 釋放一個許可
}
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
4.CyclicBarrier
4.1簡介
CyclicBarrier也是一個同步輔助類,它允許一組線程相互等待,直到到達某一個公共的屏障點;通過它可以完成多個線程之間相互等待只有當每一個想成都準備就緒後才能各自繼續往下執行後續的操作;它與CountDownLatch有一些相似的地方都是通過計數器來實現的,當某個方法調用了await()方法之後該線程就進入了等待狀態,而且計數器執行的是加一的操作,當計數器的值達到了我們所設置的初始值的時候因爲調用await()方法而進入等待狀態的線程會被喚醒繼續執行他們後續的操作,由於CyclicBarrier在釋放等待線程後可重用我們又稱之爲循環屏障;
4.2 使用場景
CyclicBarrier的使用場景與CountDownLatch比較相似,它可以用於多線程計算數據,最後合併計算結果的應用場景,比如: excel表格的總數計算;
4.3CyclicBarrier與CountDownLatch的區別
① CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以重置循環使用;
② CountDownLatch主要是實現一個或N個線程需要等待其他線程完成某項操作之後才能繼續往下執行,它描述的是一個或N個線程等待其他線程的關係;CyclicBarrier實現的是多個線程之間相互等待,直到所有的線程都滿足了條件之後,才能繼續執行後續的操作,它描述的是各個線程內部相互等待的關係;
4.4代碼演示
// CyclicBarrier 1
@Slf4j
public class CyclicBarrierExample1 {
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.error("exception", e);
}
});
}
executor.shutdown();
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
// CyclicBarrier 2
@Slf4j
public class CyclicBarrierExample2 {
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.error("exception", e);
}
});
}
executor.shutdown();
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
try {
barrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.warn("BarrierException", e);
}
log.info("{} continue", threadNum);
}
}
// CyclicBarrier 3
@Slf4j
public class CyclicBarrierExample3 {
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
log.info("callback is running");
});
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.error("exception", e);
}
});
}
executor.shutdown();
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
5.ReentrantLock
大部分前面已經介紹過了。
5.1ReentrantLock獨有的功能 :
① 可以指定公平鎖還是非公平鎖,而synchronized只能是非公平鎖;
② 提供了一個Condition的類,可以分組喚醒需要喚醒的線程,而synchronized是要麼隨機喚醒一個要麼就是全部喚醒;
③ 提供了能夠中斷等待鎖的線程的機制(lock.lockInterruptibly()來實現這個機制),ReentrantLock實現是一種自旋鎖,它通過循環調用CAS來實現加鎖,這也是它性能比較好的原因避免線程阻塞進入內核態;
5.2 代碼演示
// lock 2
@Slf4j
@ThreadSafe
public class LockExample2 {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的線程數
public static int threadTotal = 200;
public static int count = 0;
private final static Lock lock = new ReentrantLock();
public static void main(String[] args) throws Exception {
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();
}
}
}
6.ReentrantReadWriteLock
ReentrantReadWriteLock是在沒有任何讀寫鎖的時候纔可以取得寫入鎖,它裏邊有兩個鎖一個是讀鎖一個是寫鎖,這也是這個類最核心的要求同時也是我們使用它的時候需要注意的點;
/** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */ private final ReentrantReadWriteLock.WriteLock writerLock;
它可以用於實現了悲觀讀取,經常用另一個寫入的操作,但實際中應用的並不多。如果讀取較多寫入較少的情況下,使用ReentrantReadWriteLock可能會使寫入操作遭遇飢餓(也就是說寫入線程沒有辦法競爭到鎖定而一直處於等待狀態)
7.StampedLock
StampedLock控制所有三種模式分別是寫 讀 樂觀讀;StampedLock的狀態是由版本和模式兩個部分組成,鎖獲取方法返回的是一個state,它用相應的鎖狀態來表示並控制相關的訪問,數字0表示沒有寫鎖被授權訪問;
在讀鎖中分爲悲觀鎖和樂觀鎖所謂樂觀讀也就是說讀的操作很多寫的操作很少的情況下,我們可以認爲寫入和讀取同時發生的機率很小;因此不悲觀的時候讀取鎖定,程序可以查取查看讀取資料以後是否遭到寫入執行的變更在採取後續的措施,這個小改進可以大幅度提升程序的吞吐量;
樂觀鎖與悲觀鎖
// 官方實例代碼
public class LockExample4 {
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); //釋放讀鎖或寫鎖
}
}
}
}