在許久前我的博客java鎖機制中,我曾經梳理過java中的部分鎖機制,通過實驗和部分源碼閱讀來解釋原理和優劣。在此先重新梳理一遍:
1.最常用的synchronize,俗稱悲觀鎖,但凡被其修飾的代碼段(方法等)被執行時便要上鎖,這種方式可以絕對保證線性安全,但效率偏低。
2.基於CAS的樂觀鎖,數據的操作和更新分開,當代碼段執行完畢執行更新的時候,將之前複製的原值和當前值比較,如果相同就更新,比較和更新的操作是原子操作,不可分割。這樣基本線性安全且效率高,但是容易造成ABA現象。
3.基於CAS實現的重入鎖ReentrantLock,這也是我們本文的主角,在使用的時候可以起到和synchronize類似的效果,並且由於是對象實現的方式,因此比用修飾符更加靈活。此外重入鎖的最大特性當然就是在線程自身進行遞歸操作時候不會死鎖啦。
今天,我們就來看下ReentrantLock這個類吧。
由於也不是每個人都用過重入鎖,所以我這裏先以一段實例代碼開頭:
public class LockTest {
Lock lock = new ReentrantLock();
static int i = 0;
public void doSomething() {
lock.lock();
System.out.println("start doing:" + i++);
try {
Thread.sleep(10000);
System.out.println("stop doing");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTest test = new LockTest();
new Thread(new Runnable() {
@Override
public void run() {
test.doSomething();
}
}).start();
test.doSomething();
}
}
運行結果如下:
start doing:0
stop doing
start doing:1
stop doing
首先我們可以看到,lock鎖定的代碼區域並沒有被多線程同時執行,而是在前一個線程unlock了以後才執行第二個,換句話說,該類的核心方法就是lock和unlock:
public class ReentrantLock implements Lock, java.io.Serializable {
.....
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
.....
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
.....
觀察ReentrantLock類,發現其lock與unlock就是調用了sync的lock和release方法。
爲了方便閱讀我們先來解釋lock和release的含義,lock,顧名思義,加鎖,和上層調用一致,而release,之所以傳入release(1),其含義是釋放1,重入鎖之所以支持重入不造成單線程死鎖,就是因爲他的鎖不是單純地boolean類型標記,他的鎖是int標記的,在鎖尚未被佔用的時候爲0,第一個線程獲取鎖的時候會變爲1,並記錄線程號,此後如果其他線程請求獲取鎖就會進入隊列等待,而如果是佔用鎖的線程本身繼續佔用,則鎖的值會累加,直到鎖最後被釋放爲0才能被其他線程使用。因此它的上鎖是累加1的,而它的釋放自然也是。
而sync的初始化則通過傳入值分配爲公平鎖和非公平鎖。
從這裏我們不難看出這個sync需要維護幾個重要的東西:
1.這個標記鎖的值的int,這個代表了鎖是否被佔用,因此勢必是需要線程安全的,由於是鎖的源碼,不可能用鎖來保證線程安全,那麼如何保證?
2.佔用當前鎖的線程號,由於要支持重入,自然這個線程號也是需要妥善的保管的。
3.排隊的隊列。
我們現在來看sync的最基本父類:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
/** Use serial ID even though all fields transient. */
private static final long serialVersionUID = 3737899427754241961L;
/**
* Empty constructor for use by subclasses.
*/
protected AbstractOwnableSynchronizer() { }
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
/**
* Sets the thread that currently owns exclusive access.
* A {@code null} argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* {@code volatile} field accesses.
* @param thread the owner thread
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
/**
* Returns the thread last set by {@code setExclusiveOwnerThread},
* or {@code null} if never set. This method does not otherwise
* impose any synchronization or {@code volatile} field accesses.
* @return the owner thread
*/
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
這個類沒幹別的,就維護了當前線程的線程號,並提供了獲取和設置當前線程號的方法給子類使用。
這裏插播protected標識符的使用特性,當你設計一個方法只想被子類和同一個包(通常同包下是一個模塊)的其他類使用,而不想被其他模塊隨意調用就用protected。此外在抽象類的一些設計中會設計protected的抽象方法,並用public的方法去調用,這樣把不被外界直接調用的核心處理交給子類實現。
然後我們從非公平鎖的lock開始看起:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
這裏調用了原子方法compareAndSetState,由於原子方法原本就是由操作系統承諾提供的,這裏在jdk層面就不做詳細解釋了,說白了這玩意就是先比較一下當前的鎖狀態state和0是否相等,如果相等就設置爲1並調用父類方法設置當前線程的線程號;如果不是就調用acquire去累加鎖的重入層次。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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;
}
這裏是調用來調用去的,我們看到首先他先調用tryAcquire方法,而tryAcquire方法最終調用了nonfairTryAcquire方法。
在nonfairTryAcquire中:
1)首先他看了看重入程度是否爲0,這裏之所以重複比較一遍估計也是出於線程安全的考慮,如果是的話就和上一層一樣調用父類方法設置當前線程,並返回true表示已經重入成功。
2)否則的話會調用父類方法獲取佔用鎖的線程,如果這個線程就是當前線程就重入鎖(state累加),否則就返回false。
從acquire中看到在tryAcquire返回false以後鎖會調用addWaiter方法來將當前線程加入排隊隊列:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
用鏈表存儲排隊的隊列。然後返回的node用acquireQueued來執行等待:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通過一個for(;;)循環去等待,不斷獲取node的pre,如果pre已經是head說明node是第一個了,然後再調用tryAcquire,然後調用setHead將node出隊。
這基本上就是lock的主要流程。
而公平鎖與之略有區別:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
}
從代碼中我們可以看到兩點主要區別:
1)在lock時候直接acquire,而沒有先做比較state並嘗試佔用鎖的操作
2)在acquire的時候,他先保證隊列中無已在等待的線程,然後再去做比較state並佔用鎖的操作。
說白了,公平鎖必須排隊,不能先去看鎖有沒有被佔用。也就是說公平鎖保證了,不會出現A結束,B排隊的間隙,剛入隊的C插隊佔用了鎖的情況,所以是公平的。但是由於每個線程都排隊,就效率較低了。
然後在看unlock調用的release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
這裏調用了tryRelease方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelase就是,state減去要釋放的數目(這裏爲1),然後如果減到1了就設置當前線程爲null。
如果try成功就返回true。就釋放成功。
小結
本篇文章大致講述了重入鎖的實現,其中大批量使用了CAS原子方法完成。CAS的原子方法調用的都是native函數,其中和操作系統息息相關。在之前講集合類的時候也提到過jdk1.5以後出現了許多新的線程安全的集合類,是使用原子方法來完成線程安全的,這在以後可能也會通過源碼講解。