Java裏面提供了比synchronized更加靈活豐富的鎖機制,它們有一個共同的接口Lock,我們先來學習這個接口,瞭解其協議和功能。下面是JDK文檔,總結得非常精煉,包含的知識點非常多,所以一開始可能看不懂,不過沒關係,後面一點點弄懂。
public interface Lock
Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支持多個相關的 Condition 對象。
鎖是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。不過,某些鎖可能允許對共享資源併發訪問,如 ReadWriteLock 的讀取鎖。
雖然 synchronized 方法和語句的範圍機制使得使用監視器鎖編程方便了很多,而且還幫助避免了很多涉及到鎖的常見編程錯誤,但有時也需要以更爲靈活的方式使用鎖。例如,某些遍歷併發訪問的數據結果的算法要求使用 “hand-over-hand” 或 “chain locking”:獲取節點 A 的鎖,然後再獲取節點 B 的鎖,然後釋放 A 並獲取 C,然後釋放 B 並獲取 D,依此類推。Lock 接口的實現允許鎖在不同的作用範圍內獲取和釋放,並允許以任何順序獲取和釋放多個鎖,從而支持使用這種技術。
隨着靈活性的增加,也帶來了更多的責任。不使用塊結構鎖就失去了使用 synchronized 方法和語句時會出現的鎖自動釋放功能。在大多數情況下,應該使用以下語句:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
鎖定和取消鎖定出現在不同作用範圍中時,必須謹慎地確保保持鎖定時所執行的所有代碼用 try-finally 或 try-catch 加以保護,以確保在必要時釋放鎖。
Lock 實現提供了使用 synchronized 方法和語句所沒有的其他功能,包括提供了一個非塊結構的獲取鎖嘗試 (tryLock())、一個獲取可中斷鎖的嘗試 (lockInterruptibly()) 和一個獲取超時失效鎖的嘗試 (tryLock(long, TimeUnit))。
Lock 類還可以提供與隱式監視器鎖完全不同的行爲和語義,如保證排序、非重入用法或死鎖檢測。如果某個實現提供了這樣特殊的語義,則該實現必須對這些語義加以記錄。
注意,Lock 實例只是普通的對象,其本身可以在 synchronized 語句中作爲目標使用。獲取 Lock 實例的監視器鎖與調用該實例的任何 lock() 方法沒有特別的關係。爲了避免混淆,建議除了在其自身的實現中之外,決不要以這種方式使用 Lock 實例。除非另有說明,否則爲任何參數傳遞 null 值都將導致拋出 NullPointerException。
1 使用ReentrantLock進行同步
一個可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。
ReentrantLock 將由最近成功獲得鎖,並且還沒有釋放該鎖的線程所擁有。當鎖沒有被另一個線程所擁有時,調用 lock 的線程將成功獲取該鎖並返回。如果當前線程已經擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。
建議總是 立即實踐,使用 lock 塊來調用 try,在之前/之後的構造中,最典型的代碼如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
除了實現 Lock 接口,此類還定義了 isLocked 和 getLockQueueLength 方法,以及一些相關的 protected 訪問方法,這些方法對檢測和監視可能很有用。
此鎖最多支持同一個線程發起的 2147483648 個遞歸鎖。試圖超過此限制會導致由鎖方法拋出的 Error。
首先,我們先不管它有多牛逼,我們先使用它來代替synchronized實現常規的同步,也就是串行化,然後調用其中的一些方法看一看是什麼效果:
Service.java
package testReentrantLock;
import java.util.concurrent.locks.ReentrantLock;
public class Service {
private ReentrantLock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
try {
for (int i = 0; i < 3; i++) {
System.out.println("****** " + Thread.currentThread().getName() + " is printing " + i + " ******");
// 查詢當前線程保持此鎖的次數
int holdCount = lock.getHoldCount();
// 返回正等待獲取此鎖的線程估計數
int queuedLength = lock.getQueueLength();
// 如果此鎖的公平設置爲 true,則返回 true
boolean isFair = lock.isFair();
System.out.printf("---holdCount: %d;\n---queuedLength:%d;\n---isFair: %s\n\n", holdCount, queuedLength,
isFair);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
Thread1.java
package testReentrantLock;
public class Thread1 extends Thread {
private Service service;
public Thread1(Service service, String name) {
super(name);
this.service = service;
}
@Override
public void run() {
super.run();
service.testMethod();
}
public static void main(String[] args) {
Service service = new Service();
Thread1 tA = new Thread1(service, "Thread-A");
Thread1 tB = new Thread1(service, "Thread-B");
Thread1 tC = new Thread1(service, "Thread-C");
tA.start();
tB.start();
tC.start();
}
}
輸出
****** Thread-A is printing 0 ******
---holdCount: 1;
---queuedLength:2;
---isFair: false
****** Thread-A is printing 1 ******
---holdCount: 1;
---queuedLength:2;
---isFair: false
****** Thread-A is printing 2 ******
---holdCount: 1;
---queuedLength:2;
---isFair: false
****** Thread-B is printing 0 ******
---holdCount: 1;
---queuedLength:1;
---isFair: false
****** Thread-B is printing 1 ******
---holdCount: 1;
---queuedLength:1;
---isFair: false
****** Thread-B is printing 2 ******
---holdCount: 1;
---queuedLength:1;
---isFair: false
****** Thread-C is printing 0 ******
---holdCount: 1;
---queuedLength:0;
---isFair: false
****** Thread-C is printing 1 ******
---holdCount: 1;
---queuedLength:0;
---isFair: false
****** Thread-C is printing 2 ******
---holdCount: 1;
---queuedLength:0;
---isFair: false
我稍微囉嗦地解釋一下getHoldCount,它返回的是查詢當前線程保存此lock的個數,也就是在此線程代碼內,代用lock.lock() 的次數。一般一個線程內每個需要同步的代碼塊就會使用鎖定嘛:
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
2 使用Condition實現等待/通知
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,爲每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。
條件(也稱爲條件隊列 或條件變量)爲線程提供了一個含義,以便在某個狀態條件現在可能爲 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因爲訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式 釋放相關的鎖,並掛起當前線程,就像 Object.wait 做的那樣。Condition 實例實質上被綁定到一個鎖上。要爲特定 Lock 實例獲得 Condition 實例,請使用其 newCondition() 方法。
看完下面的這個例子你就會使用Condition了。
BoundedBuffer.java
package testReentrantLock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
final ReentrantLock lock = new ReentrantLock();
// notFull 才能put
final Condition notFull = lock.newCondition();
// notEmpty 才能take
final Condition notEmpty = lock.newCondition();
final int[] items = new int[2];
int putptr, takeptr, count;
public void put(int x) throws InterruptedException {
// 每次put之前線程得獲得這個鎖才行
lock.lock();
try {
// 如果是full,則讓這個企圖put的線程等待
while (count == items.length) {
System.out.printf("----FULL---- The buffer is full! %s has to wait.\n",
Thread.currentThread().getName());
notFull.await();
}
// 每次只要put成功,則通知一下 notEmpty,如果存在等待take的線程,則喚醒一個讓它取
items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.printf("----EMPTY---- The buffer is empty! %s has to wait.\n",
Thread.currentThread().getName());
notEmpty.await();
}
// 每次take成功,則通知 notFull,如果有等待put的線程,則讓它放
int x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
BufferThread.java
package testReentrantLock;
public class BufferThread extends Thread {
private BoundedBuffer boundedBuffer = new BoundedBuffer();
private String name;
public BufferThread(BoundedBuffer boundedBuffer, String name) {
super(name);
this.boundedBuffer = boundedBuffer;
this.name = name;
}
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + " is running!");
if (name.startsWith("PUT")) {
for (int i = 1; i < 4; i++) {
try {
boundedBuffer.put(i);
System.out.printf("--PUT-- %s has put %d into the buffer.\n", Thread.currentThread().getName(), i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else if (name.startsWith("TAKE")) {
for (int i = 1; i < 4; i++) {
try {
int value = boundedBuffer.take();
System.out.printf("--TAK-- %s has took %d from the buffer.\n", Thread.currentThread().getName(),
value);
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BoundedBuffer boundedBuffer = new BoundedBuffer();
// 創建3個put線程,每個往Buffer裏put 3次
BufferThread put1 = new BufferThread(boundedBuffer, "PUT1");
BufferThread put2 = new BufferThread(boundedBuffer, "PUT2");
BufferThread put3 = new BufferThread(boundedBuffer, "PUT3");
// 創建2個take線程,每個從Buffer裏take 3次
BufferThread take1 = new BufferThread(boundedBuffer, "TAKE1");
BufferThread take2 = new BufferThread(boundedBuffer, "TAKE2");
put1.start();
put2.start();
put3.start();
take1.start();
take2.start();
}
}
輸出
PUT2 is running!
TAKE1 is running!
TAKE2 is running!
----EMPTY---- The buffer is empty! TAKE2 has to wait.
PUT1 is running!
PUT3 is running!
--PUT-- PUT3 has put 1 into the buffer.
--PUT-- PUT2 has put 1 into the buffer.
--TAK-- TAKE1 has took 1 from the buffer.
--TAK-- TAKE2 has took 1 from the buffer.
--PUT-- PUT1 has put 1 into the buffer.
--TAK-- TAKE2 has took 1 from the buffer.
----EMPTY---- The buffer is empty! TAKE1 has to wait.
--PUT-- PUT3 has put 2 into the buffer.
----FULL---- The buffer is full! PUT2 has to wait.
--PUT-- PUT1 has put 2 into the buffer.
--PUT-- PUT2 has put 2 into the buffer.
--TAK-- TAKE1 has took 2 from the buffer.
--TAK-- TAKE2 has took 2 from the buffer.
--TAK-- TAKE1 has took 2 from the buffer.
--PUT-- PUT3 has put 3 into the buffer.
--PUT-- PUT1 has put 3 into the buffer.
----FULL---- The buffer is full! PUT2 has to wait.
使用Condition的優越性就在於,它把等待的線程分類了,利用同一個lock創建不同的Condition,你想把等待的線程分成幾類你就創建多少個Condition就好了。在特定條件下,喚醒不同類別的等待線程,多麼方便。如果這樣說你還是不明白Condition的優越性,那麼看看同樣的功能使用synchronized編寫是怎麼樣的:
對BoundedBuffer.java的改寫:
package testReentrantLock;
public class BoundedBufferSyn {
final int[] items = new int[2];
int putptr, takeptr, count;
synchronized public void put(int x) throws InterruptedException {
// 如果是full,則讓這個企圖put的線程等待
while (count == items.length) {
System.out.printf("----FULL---- The buffer is full! %s has to wait.\n", Thread.currentThread().getName());
// 這裏的wait和Condition的await在功能上沒有什麼區別,重點在喚醒
wait();
}
// 每次只要put成功,則通知一下 notEmpty,如果存在等待take的線程,則喚醒一個讓它取
items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
// 喚醒所有等待線程,讓它們再去搶一次鎖,而無法只通知特性的線程
notifyAll();
}
synchronized public int take() throws InterruptedException {
while (count == 0) {
System.out.printf("----EMPTY---- The buffer is empty! %s has to wait.\n",
Thread.currentThread().getName());
wait();
}
// 每次take成功,則通知 notFull,如果有等待put的線程,則讓它放
int x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notifyAll();
return x;
}
}
3 公平鎖
ReentrantLock的公平鎖是個啥? 先來看看JDK文檔的解釋:
此類的構造方法接受一個可選的公平 參數。當設置爲 true 時,在多個線程的爭用下,這些鎖【傾向於】將訪問權授予等待時間最長的線程。否則此鎖將無法保證任何特定訪問順序。與採用默認設置(使用不公平鎖)相比,使用公平鎖的程序在許多線程訪問時【表現爲很低的總體吞吐量(即速度很慢,常常極其慢)】,但是在獲得鎖和保證鎖分配的均衡性時差異較小。
【不過要注意的是,公平鎖不能保證線程調度的公平性。】因此,使用公平鎖的衆多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時。還要注意的是,未定時的 tryLock 方法並沒有使用公平設置。因爲即使其他線程正在等待,只要該鎖是可用的,此方法就可以獲得成功。
關於公平鎖,我覺得文檔已經解釋的非常清楚了,我就不編寫示例代碼了。