自 Java 5 開始,java.util.concurrent.locks 套件中包含了一些鎖定的實現,因此你不用去實現自己的鎖定了。但是你仍然需要去了解怎麼使用這些鎖。
一個簡單的鎖
讓我們從 java 中的一個同步區塊開始:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
可以看到在 inc()方法中有一個 synchronized(this)程式碼區塊。該程式碼區塊可以保證在同一時間只有一個執行緒可以執行 return ++count。雖然在 synchronized 的同步區塊中的程式碼可以更加複雜,但是++count 這種簡單的操作已經足以表達出線程同步的意思。
以下的Counter 類別以Lock 取代synchronized 達到了同樣的目的:
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; } }
lock()方法會對Lock 實例物件進行加鎖,因此所有對該物件呼叫lock()方法的執行緒都會被阻塞,直到該Lock 物件的unlock()方法被呼叫。
這裡有一個 Lock 類別的簡單實作:
public class Counter{ public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); } }
注意其中的 while(isLocked)循環,它又被叫做「自旋鎖」。當 isLocked 為 true 時,呼叫 lock()的執行緒在 wait()呼叫上阻塞等待。為防止該執行緒沒有收到notify()呼叫也從wait()中傳回(也稱為虛假喚醒),這個執行緒會重新去檢查isLocked 條件以決定目前是否可以安全地繼續執行還是需要重新保持等待,而不是認為執行緒被喚醒了就可以安全地繼續執行了。如果 isLocked 為 false,目前執行緒會退出 while(isLocked)循環,並將 isLocked 設回 true,讓其它正在呼叫 lock()方法的執行緒能夠在 Lock 實例上加鎖。
當執行緒完成了臨界區(位於 lock()和 unlock()之間)中的程式碼,就會呼叫 unlock()。執行 unlock()會重新將 isLocked 設為 false,並且通知(喚醒)其中一個(若有的話)在 lock()方法中呼叫了 wait()函數而處於等待狀態的執行緒。
鎖的可重入性
Java 中的 synchronized 同步區塊是可重入的。這意味著如果一個java 執行緒進入了程式碼中的synchronized 同步區塊,並因此獲得了該同步區塊所使用的同步物件對應的管程上的鎖,那麼這個執行緒可以進入由同一個管程物件所同步的另一個java 程式碼塊。以下是一個例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意 outer()和 inner()都被宣告為 synchronized,這在 Java 中和 synchronized(this)區塊等效。如果一個執行緒呼叫了 outer(),在 outer()裡呼叫 inner()就沒有什麼問題,因為這兩個方法(程式碼區塊)都由同一個管程物件(”this”)所同步。如果一個執行緒已經擁有了一個管程物件上的鎖,那麼它就有權存取被這個管程物件同步的所有程式碼區塊。這就是可重入。執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼區塊。
前面給出的鎖實現不是可重入的。如果我們像下面這樣重寫 Reentrant 類,當執行緒呼叫 outer()時,會在 inner()方法的 lock.lock()處阻塞住。
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); } }
呼叫 outer()的執行緒會先鎖住 Lock 實例,然後繼續呼叫 inner()。 inner()方法中該執行緒將再一次嘗試鎖定 Lock 實例,結果該動作會失敗(也就是說該執行緒會被阻塞),因為這個 Lock 實例已經在 outer()方法中被鎖定了。
兩次lock()之間沒有呼叫unlock(),第二次呼叫lock 就會阻塞,看過lock()實作後,會發現原因很明顯:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
一個執行緒是否被允許退出lock()方法是由while 循環(自旋鎖)中的條件決定的。目前的判斷條件是只有當 isLocked 為 false 時 lock 操作才被允許,而沒有考慮是哪個執行緒鎖住了它。
為了讓這個 Lock 類別具有可重入性,我們需要對它做一點小的改動:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.curentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } ... }
注意到現在的 while 循環(自旋鎖)也考慮到了已鎖住該 Lock 實例的線程。如果目前的鎖定物件沒有被加鎖(isLocked = false),或是目前呼叫執行緒已經對該Lock 實例加了鎖,那麼while 迴圈就不會被執行,而呼叫lock()的執行緒就可以退出該方法(譯者註:「被允許退出該方法」在目前語意下就是指不會呼叫wait()而導致阻塞)。
除此之外,我們需要記錄同一個執行緒重複對一個鎖定物件加鎖的次數。否則,一次 unblock()呼叫就會解除整個鎖,即使目前鎖已經被加鎖過多次。在 unlock()呼叫沒有達到對應 lock()呼叫的次數之前,我們不希望鎖被解除。
現在這個 Lock 類別就是可重入的了。
鎖的公平性
Java 的 synchronized 區塊並不保證嘗試進入它們的執行緒的順序。因此,如果多個執行緒不斷競爭存取相同的 synchronized 同步區塊,就存在一種風險,其中一個或多個執行緒永遠也無法獲得存取權 —— 也就是說存取權總是分配給了其它執行緒。這種情況被稱作線程飢餓。為了避免這種問題,鎖需要實現公平性。本文所展現的鎖在內部是用 synchronized 同步塊實現的,因此它們也不保證公平性。
在 finally 语句中调用 unlock()
如果用 Lock 来保护临界区,并且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:
lock.lock(); try{ //do critical section code, //which may throw exception } finally { lock.unlock(); }
这个简单的结构可以保证当临界区抛出异常时 Lock 对象可以被解锁。如果不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会导致其它所有在该 Lock 对象上调用 lock()的线程一直阻塞。
以上就是关于 java 多线程锁的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!
更多java 多线程-锁详解及示例代码相关文章请关注PHP中文网!