本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。
Lock接口簡介
在JUC包下面有一個java.util.concurrent.locks
包,這個包提供了一系列基礎的鎖工具,對傳統的synchronizd、wait和notify等同步機制進行補充和增強。下面先來介紹下這個Lock接口。
Lock
接口可以視為synchronized
的增強版,提供了更靈活的功能。相對於synchronized
,Lock
接口還提供了限時鎖等待、鎖中斷和鎖嘗試等功能。
該接口的定義如下
public interface Lock {
// 嘗試去獲得鎖
// 如果鎖不可用,當前線程會變得不可用,直到獲得鎖為止。(中途會忽略中斷)
void lock();
// 嘗試去獲取鎖,如果鎖獲取不到,線程將不可用
// 直到獲取鎖,或者被其他線程中斷
// 線程在獲取鎖操作中,被其他線程中斷,則會拋出InterruptedException異常,並且將中斷標識清除。
void lockInterruptibly() throws InterruptedException;
// 鎖空閑時返回true,鎖不空閑是返回false
// 該方法不會引起當前線程阻塞
boolean tryLock();
// 在unit時間內成功獲取鎖,返回true
// 在unit時間內未成功獲取鎖,返回false
// 如果當前線程在獲取鎖操作中,被其他線程中斷,則會拋出InterruptedException異常,並且將中斷標識清除。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 釋放鎖
void unlock();
// 獲取一個綁定到當前Lock對象的Condition對象
// 獲取Condition對象的前提是當前線程持有Lock對象
Condition newCondition();
}
關於上面的lock()和lockInterruptibly()方法,有如下區別:
lock()方法類似於使用synchronized關鍵字加鎖,如果鎖不可用,出於線程調度目的,將禁用當前線程,並且在獲得鎖之前,該線程將一直處於休眠狀態。
lockInterruptibly()方法顧名思義,就是如果鎖不可用,那么當前正在等待的線程是可以被中斷的,這比synchronized關鍵字更加靈活。
Lock接口的經典用法
Lock lock = new ReentrantLock();
//嘗試獲取鎖,如果當前該鎖沒有被其他線程持有,則當前線程獲取該鎖並返回true,否則返回false。
//該方法不會引起當前線程阻塞
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
或者是
lock.lock()
try {
// manipulate protected state
} finally {
lock.unlock();
}
這邊不要將獲取鎖的過程寫在try塊中,因為如果在獲取鎖(自定義鎖的實現)時發生了異常,異常拋出的同時,也會導致鎖無故釋放。
ReentrantLock
ReentrantLock
類是一個可重入的獨占鎖,除了具有和synchronized一樣的功能外,還具有限時鎖等待、鎖中斷和鎖嘗試等功能。
ReentrantLock
底層是通過繼承AQS來實現獨占鎖功能的。
公平鎖和非公平鎖
關於ReentrantLock
,有兩個很重要的概念需要學習:公平鎖和非公平鎖。
查看ReentrantLock
的源代碼,我們會看到兩個構造函數,分為對應構造公平鎖和非公平鎖。
//默認構造非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
//true構造公平鎖,false構造非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平鎖:是指線程在搶占鎖失敗后會進入一個等待隊列,先進入隊列的線程會先獲得鎖。公平性體現在先來先得。
非公平鎖:是指線程搶占鎖失敗后會進入一個等待隊列,但是這些等待線程誰能先獲得鎖不是按照先來先得的規則,而是隨機的。不公平性體現在后來的線程可能先得到鎖。
如果有很多線程競爭一把公平鎖,系統的總體吞吐量(即速度很慢,常常極其慢)比較低,因為此時在線程調度上面的開銷比較大。
原因是采用公平策略時,當一個線程釋放鎖時,需要先將等待隊列中的線程喚醒。這個喚醒的調度過程是比較耗費時間的。如果使用非公平鎖的話,當一個線程釋放鎖之后,可用的線程能立馬獲得鎖,效率較高。
ReentrantLock代碼實現
1. 非公平鎖代碼
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//如果沒有線程占據鎖,則占據鎖,也就是將state從0設置為1
//這種搶占方式不要排隊,有人釋放了鎖,你可以直接插到第一位
//去搶,只要你能搶到
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否則嘗試搶占鎖
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
通過之前對AQS的介紹,我們知道搶占鎖的時候會調用 tryAcquire 方法。非公平鎖的這個方法直接調用了父類中的nonfairTryAcquire
。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//鎖已經被釋放,則直接占據鎖
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//否則判斷鎖是不是之前被自己占用過,並設置重入次數
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;
}
1. 公平鎖代碼
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//沒人在隊列中排隊,並且鎖已經被釋放才能搶占到鎖,否則去隊列中排隊
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//設置重入次數
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}