Lock跟Java中的synchronized
關鍵字同樣,都是用於線程的同步機制。不一樣的是Lock相比synchronized
關鍵字提供更加豐富的功能和靈活性。html
從Java5開始,java.util.concurrent.locks
包中提供了幾種不一樣的Lock實現,所以咱們不須要本身去實現鎖。但咱們仍然須要知道怎麼使用它們,以及它們的底層原理。java
咱們來看一下這樣一個同步塊:安全
public class Counter {
private int count = 0;
public int increment() {
synchronized (this) {
return ++count;
}
}
}
複製代碼
咱們能夠注意到increment()方法中的同步塊。這個同步塊確保了每次只會有一個線程執行++count;
同步塊的中的代碼相對比較簡單,如今咱們只須要關注++count();
這一行代碼便可。post
上面的例子用Lock來替換同步塊:this
public class Counter {
private Lock lock = new Lock();
private int count = 0;
public int increment() {
lock.lock();
return ++count;
lock.unlock();
}
}
複製代碼
increment()方法中,當有線程取得Lock實例後,其餘線程都會在調用lock()後被阻塞,直到取得Lock實例的線程調用了unlock()爲止。spa
下面是一個簡單的Lock實現:線程
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),咱們稱這種循環爲"旋轉鎖"。旋轉鎖以及wait()和notify()方法調用在線程通信一文中有說起。當isLocked爲true時,線程會進入循環內部,從而調用wait()方法進入等待狀態。實例中,在其餘線程沒有調用notify()發送信號喚醒的狀況下,線程如果意外喚醒會從新檢查isLocked條件是否爲false以此來肯定線程是否能夠安全的執行,若仍然爲true則線程從新調用wait()方法進入等待狀態。所以旋轉鎖能夠保護意外喚醒帶來的潛在風險。若線程檢查isLocked爲false則退出while循環,將isLocked置換爲true,以此來標記得到當前Lock實例。讓其餘線程阻塞在lock()調用上。code
當線程執行完臨界區代碼(位於lock()和unlock()調用之間的代碼)後,會調用unlock()方法釋放Lock()實例。調用unlock()方法會將isLocked置換爲false.並喚醒調用了lock()方法中的wait()方法進入等待狀態的線程,若是有的話。htm
Java中的synchronized
同步塊是可重入的。這意味着,若是一個Java線程進入一個同步代碼塊取得當前對象鎖,那麼它能夠再進入其餘所用與之相同對象鎖的其餘同步代碼塊。以下所示:對象
public class Reentrant{
public synchronized void outer() {
inner();
}
public synchronized void inner() {
// do something you like
}
}
複製代碼
咱們能夠注意到outer()和inner()方法簽名中都有synchronized
聲明,這在Java中等同於synchronized(this)
同步代碼塊。當多個方法都是以"this"即當前對象做爲監控對象時,那麼一個線程在調用完outer()方法後,天然能夠在outer()方法內部調用inner()方法。一個線程將一個對象做爲監控對象即取得該對象鎖後,它能夠進入其餘使用相同對象做爲對象鎖的同步代碼塊。這種狀況咱們稱之爲可重入。線程能夠反覆進入它所取得監控對象的所有同步代碼塊。
上文中給出的Lock實現並非可重入的。若是咱們將上文的Reentrant.class改寫成以下代碼所示的Reentrant2.class,那麼線程在調用完outer()方法後,將在調用inner()方法中的lock.lock()
方法後阻塞。
public class Reentrant2{
Lock lock = new Lock();
public void outer() {
lock.lock();
inner();
lock.unlock();
}
public synchronized void inner() {
lock.lock();
//do something
lock.unlock();
}
}
複製代碼
線程在調用outer()時會先鎖住Lock實例。而後再調用inner()方法。在inner()方法內部,線程會再一次嘗試鎖住Lock實例,但註定會失敗。由於lock實例在調用outer()方法時已經被鎖住。
在尚未調用unlock()方法釋放Lock()實例的時候,再次調用lock()的時候會讓線程阻塞在調用lock()方法上。以下lock()方法:
public class Lock{
boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
}
複製代碼
旋轉鎖中的條件,決定了線程允不容許退出lock()方法。當isLocked爲false時,意味着線程容許退出lock()方法,即容許鎖住Lock實例。
讓Lock.class支持可重入,咱們須要做出一點改動:
public class Lock {
private boolean isLocked = false;
private Thread lockingThread;
int lockedCount = 0;
public void lock() throws InterruptedException {
Thread callingThread = Thread.currentThread();
while (isLocked && lockingThread != callingThread) {
wait();
}
isLocked = true;
lockingThread = callingThread;
lockedCount++;
}
public void unlock() {
Thread callingThread = Thread.currentThread();
if (lockingThread != null && lockingThread == callingThread) {
lockedCount--;
if (lockedCount == 0) {
isLocked = false;
notify();
}
}
}
}
複製代碼
咱們能夠注意到while循環(旋轉鎖)中多了一個條件,即當前線程是否爲持有鎖實例的線程。當只有兩個條件都符合時線程才能退出循環和退出lock()方法調用。
咱們須要對線程鎖住Lock實例的次數進行記錄。不然的話,調用一次unlock()方法就會讓當前線程釋放掉鎖儘管實際上它調用了屢次lock()。所以咱們須要保障lock()和unlock()方法的調用次數是一致的,即每調用一次lock()即必須調用一次unlock()。
如今的Lock.class已是可重入的了。
Java的synchronized
同步代碼塊不能保證線程按照必定的順序進入同步代碼塊。所以存在必定的風險,有一或者幾個線程一直沒法進入同步代碼塊,由於其餘線程一直代替它們進入到同步代碼塊。咱們稱這種狀況爲肌餓現象。
爲了解決這個問題,咱們實現的鎖須要保證必定的公平性,但上文給出的Lock實現並不能保證線程的公平性。飢餓和公平一文中詳細討論了這種狀況。
當使用Lock來保證臨界區代碼的同步時,臨界區中的代碼可能會拋出異常。因此頗有必要在finally語句中來調用unlock()方法。不管線程執行的代碼異常與否,始終可以釋放它所持有的鎖,以讓其餘線程能正常執行。以下所示:
lock.lock();
try {
//do critical section code, which may throw exception
} finally {
lock.unlock();
}
複製代碼
咱們使用這個小結構來保證即便臨界區代碼拋出異常,線程也能正常釋放掉所持有的鎖。若是咱們沒有這麼作,一旦臨界區代碼出現異常,線程將沒法釋放鎖,以致於其餘調用了該鎖的lock()方法的線程將會永遠等待下去。
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial