上一篇文章中,我們已經介紹過了各種鎖,讓各位對鎖有了一定的了解。接下來將為各位介紹鎖在Java中的實現。關注我的公眾號「Java面典」了解更多 Java 相關知識點。
在 Java 中主要通過使用synchronized 、 volatile關鍵字,及 Lock 接口的子類 ReentrantLock 和 ReadWriteLock 等來實現加鎖。
synchronized
屬性
synchronized 屬於獨占式的悲觀鎖,同時屬於可重入鎖。
作用
synchronized 可以把任意一個非 NULL 的對象當作鎖。其在不同場景下的作用范圍如下:
- 作用於方法時,鎖住的是對象的實例(this);
- 作用於靜態方法時,鎖住的是Class實例,會鎖住所有調用該方法的線程。(又因為Class的相關數據存儲在永久代 PermGen【Jdk1.8 則是 metaspace】,永久代是全局共享的,因此靜態方法鎖相當於類的一個全局鎖);
- 作用於一個對象實例時,鎖住的是所有以該對象為鎖的代碼塊。
實現
它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。
- Wait Set:存儲調用 wait 方法被阻塞的線程;
- Contention List(競爭隊列):所有請求鎖的線程首先被放在這個競爭隊列中;
- Entry List:Contention List 中那些有資格成為候選資源的線程被移動到 Entry List 中;
- OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程成為 OnDeck;
- Owner:當前已經獲取到所資源的線程被稱為 Owner;
- !Owner:當前釋放鎖的線程。
volatile
屬性
比 sychronized 更輕量級的同步鎖
適用場景
使用 volatile 必須同時滿足下面兩個條件才能保證在並發環境的線程安全:
- 對變量的寫操作不依賴於當前值(比如 i++),或者說是單純的變量賦值(boolean flag = true);
- 不同的 volatile 變量之間,不能互相依賴,只有在狀態真正獨立於程序內其他內容時才能使用 volatile。
對 volatile 變量的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變量,但是並不能保證 i++ 這種操作的原子性,因為本質上 i++ 是讀、寫兩次操作。
Lock
Java 中的鎖都實現於 Lock 接口,主要方法有:
- void lock(): 用於獲取鎖。如果鎖可用,則獲取鎖。 若鎖不可用, 將禁用當前線程,直到取到鎖;
- boolean tryLock():嘗試獲取鎖。如果鎖可用,則獲取鎖,並返回 true, 否則返回 false;
該方法和lock()的區別在於,如果鎖不可用,tryLock()不會導致當前線程被禁用。
- tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖。
- void unlock():釋放鎖。鎖只能由持有者釋放,如果線程並不持有鎖,卻執行該方法。可能導致異常的發生;
- Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的 await()方法,而調用后,當前線程將縮放鎖;
- getHoldCount() :查詢當前線程保持此鎖的次數。
- getQueueLength():返回正等待獲取此鎖的線程估計數。比如啟動 10 個線程,1 個線程獲得鎖,此時返回的是 9;
- getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線程估計數。比如 10 個線程,用同一個 condition 對象,並且此時這 10 個線程都執行了condition 對象的 await 方法,那么此時執行此方法返回 10;
- hasWaiters(Condition condition):查詢是否有線程等待與此鎖有關的給定條件(condition)。對於指定 contidion 對象,有多少線程執行了 condition.await 方法;
- hasQueuedThread(Thread thread):查詢給定線程是否等待獲取此鎖;
- hasQueuedThreads():是否有線程等待此鎖;
- isFair():該鎖是否公平鎖;
- isHeldByCurrentThread(): 當前線程是否保持鎖鎖定,線程的執行 lock 方法的前后分別是 false 和 true;
- isLock():此鎖是否有任意線程占用;
- lockInterruptibly():如果當前線程未被中斷,獲取鎖。
tryLock 和 lock 和 lockInterruptibly 的區別
- tryLock 能獲得鎖就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false;
- lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖;
- lock 和 lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程,lock 不會拋出異常,而 lockInterruptibly 會拋出異常。
Condition
作用
Condition 的作用是對鎖進行更精確的控制。對於同一個鎖,我們可以創建多個 Condition,在不同的情況下使用不同的 Condition。
Condition 和 Object
- 相似之處:
- Condition 類的 awiat 方法和 Object 類的 wait 方法等效;
- Condition 類的 signal 方法和 Object 類的 notify 方法等效;
- Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效。
- 不同處:
- ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的;
- Object中的 wait()、notify()、notifyAll() 方法是和 "同步鎖"(synchronized關鍵字) 捆綁使用的;而Condition是需要與 "互斥鎖"/"共享鎖" 捆綁使用的。
ReentrantLock
屬性
可重入鎖。
特點
除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。
與 synchronized 的區別
- ReentrantLock 需要通過方法 lock() 與 unlock() 手動進行加鎖與解鎖操作,而 synchronized 會 被 JVM 自動加鎖、解鎖;
- ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。
public class MyLock {
private Lock lock = new ReentrantLock();
// Lock lock = new ReentrantLock(true); //公平鎖
// Lock lock = new ReentrantLock(false); //非公平鎖
private Condition condition = lock.newCondition(); //創建 Condition
public void testMethod() {
try {
lock.lock(); //lock 加鎖
// 1:wait 方法等待:
//System.out.println("開始 wait");
condition.await();
// 通過創建 Condition 對象來使線程 wait,必須先執行 lock.lock 方法獲得鎖
// 2:signal 方法喚醒
condition.signal(); //condition 對象的 signal 方法可以喚醒 wait 線程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ReadWriteLock
屬性
共享鎖(讀-寫鎖)
特點
- 如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率;
- 讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥(這是由 JVM 自己控制的,代碼只要上好相應的鎖即可)。
使用原則
- 如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖;
- 如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。
CountDownLatch(線程計數器 )
作用
CountDownLatch 是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。
final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
public void run() {
System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
Thread.sleep(3000);
System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
latch.countDown();
}
;
}.start();
new Thread() {
public void run() {
System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
Thread.sleep(3000);
System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
latch.countDown();
}
;
}.start();
System.out.println("等待 2 個子線程執行完畢...");
latch.await();
System.out.println("2 個子線程已經執行完畢");
System.out.println("繼續執行主線程");
CyclicBarrierr(回環柵欄-等待至 barrier 狀態再全部同時執行)
作用
CyclicBarrier 是一個同步輔助類,允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)。因為該 barrier 在釋放等待線程后可以重用,所以稱它為循環 的 barrier。
主要方法
CyclicBarrier 中最重要的方法就是 await 方法,它有 2 個重載版本:
- public int await():用來掛起當前線程,直至所有線程都到達 barrier 狀態再同時執行后續任務;
- public int await(long timeout, TimeUnit unit):讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀態就直接讓到達 barrier 的線程執行后續任務。
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for (int i = 0; i < N; i++)
new Writer(barrier).start();
}
static class Writer extends Thread {
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
Thread.sleep(5000); //以睡眠來模擬線程需要預定寫入數據操作
System.out.println("線程" + Thread.currentThread().getName() + "寫入數據完 畢,等待其他線程寫入完畢");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有線程寫入完畢,繼續處理其他任務,比如數據操作");
}
}
CountDownLatch 和 CyclicBarrier 的區別
- CountDownLatch 的作用是允許 1 或 N 個線程等待其他線程完成執行;而 CyclicBarrier 則是允許 N 個線程相互等待;
- CountDownLatch 的計數器無法被重置;CyclicBarrier 的計數器可以被重置后使用,因此它被稱為是循環的 barrier。
Semaphore
Semaphore 是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做完自己的申請后歸還,超過閾值后,線程申請許可信號將會被阻塞。Semaphore 可以用來構建一些對象池,資源池之類的,比如數據庫連接池。
實現互斥鎖(計數器為 1)
我們也可以創建計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。
代碼實現
它的用法如下:
// 創建一個計數閾值為 5 的信號量對象
// 只能 5 個線程同時訪問
Semaphore semp = new Semaphore(5);
try { // 申請許可
semp.acquire();
try {
// 業務邏輯
} catch (Exception e) {
} finally {
// 釋放許可
semp.release();
}
} catch (InterruptedException e) {
}
Semaphore 與 ReentrantLock 相似處
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也有許多類似之處:
- 都需要手動加鎖。通過 acquire() 與 release() 方法來獲得和釋放資源;
- Semaphone.acquire()方法默認為可響應中斷鎖,與 ReentrantLock.lockInterruptibly() 作用效果一致,也就是說在等待臨界資源的過程中可以被 Thread.interrupt() 方法中斷;
- Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock不同,其使用方法與 ReentrantLock 幾乎一致;
- Semaphore 也提供了公平與非公平鎖的機制,也可在構造函數中進行設定;
- 鎖釋放方式相同。與 ReentrantLock 一樣 Semaphore 的鎖釋放操作也由手動進行。同時,為避免線程因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 代碼塊中完成。
多線程與並發系列推薦
Java多線程並發03——什么是線程上下文,線程是如何調度的