之前總結了部分無鎖機制的多線程基礎,理想的狀態當然是利用無鎖同步解決多線程程序設計的問題。但是實際碰到的問題使得很多情 況下,我們不得不借助鎖同步來保證線程安全。自從JDK5開始,有兩種機制來屏蔽代碼塊在並行訪問的干擾,synchronized關鍵字已經介紹 過了部分內容,所以這次簡單的說說另一種鎖機制:ReentrantLock。
對於synchronized的缺點之前也簡單的說了一些,實際使用中比較煩擾的幾點是:a.只有一個"條件"與鎖相關聯,這對於大量並發線程 的情況是很難管理(等待和喚醒);b.多線程競爭一個鎖時,其余未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。這種情況對於大量的 競爭線程會造成性能的下降等後果。JDK5以後提供了ReentrantLock的同步機制對於前面提的兩種情況有相對的改善。下面我還是寫個小例 子分析一下:
Java代碼
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: yanxuxin
* @date: 2010-1-4
*/
public class ReentrantLockSample {
public static void main(String[] args) {
testSynchronized();
testReentrantLock();
}
public static void testReentrantLock() {
final SampleSupport1 support = new SampleSupport1();
Thread first = new Thread(new Runnable() {
public void run() {
try {
support.doSomething();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread second = new Thread(new Runnable() {
public void run() {
try {
support.doSomething();
}
catch (InterruptedException e) {
System.out.println("Second Thread Interrupted without executing counter++,beacuse it waits a long time.");
}
}
});
executeTest(first, second);
}
public static void testSynchronized() {
final SampleSupport2 support2 = new SampleSupport2();
Runnable runnable = new Runnable() {
public void run() {
support2.doSomething();
}
};
Thread third = new Thread(runnable);
Thread fourth = new Thread(runnable);
executeTest(third, fourth);
}
/**
* Make thread a run faster than thread b,
* then thread b will be interruted after about 1s.
* @param a
* @param b
*/
public static void executeTest(Thread a, Thread b) {
a.start();
try {
Thread.sleep(100);
b.start(); // The main thread sleep 100ms, and then start the second thread.
Thread.sleep(1000);
// 1s later, the main thread decided not to allow the second thread wait any longer.
b.interrupt();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
abstract class SampleSupport {
protected int counter;
/**
* A simple countdown,it will stop after about 5s.
*/
public void startTheCountdown() {
long currentTime = System.currentTimeMillis();
for (;;) {
long diff = System.currentTimeMillis() - currentTime;
if (diff > 5000) {
break;
}
}
}
}
class SampleSupport1 extends SampleSupport {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() throws InterruptedException {
lock.lockInterruptibly(); // (1)
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
try {
counter++;
}
finally {
lock.unlock();
}
}
}
class SampleSupport2 extends SampleSupport {
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
counter++;
}
}
在這個例子中,輔助類SampleSupport提供一個倒計時的功能startTheCountdown(),這裡倒計時5s左右。 SampleSupport1,SampleSupport2繼承其並分別的具有doSomething()方法,任何進入方法的線程會運行5s左右之後 counter++然後離開方法 釋放鎖。SampleSupport1是使用ReentrantLock機制,SampleSupport2是使用 synchronized機制。
testSynchronized()和testReentrantLock()都分別開啟兩個線程執行測試方法executeTest(),這個方法會讓一個線程先啟動,另一個 過100ms左右啟動,並且隔1s左右試圖中斷後者。結果正如之前提到的第二點:interrupt()對於 synchronized是沒有作用的,它依然會等待 5s左右獲得鎖執行counter++;而ReentrantLock機制可以保證在線程還未獲得並且試圖獲得鎖時如果發現線程中斷,則拋出異常清除中斷標 記退出競爭。所以testReentrantLock()中second線程不會繼續去競爭鎖,執行異常內的打印語句後線程運行結束。
這裡我是用了ReentrantLock的lockInterruptibly()方法,在SampleSupport1的代碼(1)處。這個方法保證了中斷線程的響應,如果僅僅 是lock()則不會有此功能。但是不管怎麼說ReentrantLock提供了解決方案。至於提到的第一點“多條件”的機制我通過 java.util.concurrent.ArrayBlockingQueue(源碼參考1.6.0.17內的實現)簡單的介紹一下:
Java代碼
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
...
/** Main lock guarding all access */
private final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
...
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = (E[]) new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final E[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == items.length)
notFull.await();
} catch (InterruptedException ie) {
notFull.signal(); // propagate to non-interrupted thread
throw ie;
}
insert(e);
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
throw ie;
}
E x = extract();
return x;
} finally {
lock.unlock();
}
}
private E extract() {
final E[] items = this.items;
E x = items[takeIndex];
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
...
}
這裡notEmpty和notFull作為lock的兩個條件是可以分別負責管理想要加入元素的線程和想要取出元素的線程的wait和notify分別通過 await()和signal(),signalAll()方法,有效的分離了不同職責的線程。例如put()方法在元素個數達到最大限制時會使用 notFull條件把試 圖繼續插入元素的線程都扔到等待集中,而執行了take()方法時如果順利進入extract()則會空出空間,這時 notFull負責隨機的通知被其 扔到等待集中的線程執行插入元素的操作。這樣的設計使得線程按照功能行為職責管理成為了現實。
通過上述的總結,對於ReentrantLock的優點有了一定的認識,但是它也是實現了與synchronized相同語義和行為的可重用完全互斥鎖, 所以在競爭機制上不會有什麼性能提高,功能倒是強大了不少。不過使用它要配合try{...}finally{...}顯式的釋放鎖,這點是決定如果業 務實現沒有需要使用其特有的功能,更好的方式是使用synchronized。後者畢竟不用自己去釋放鎖,降低了開發的失誤率。當然在 java.util.concurrent.locks包內還一個很有意思的鎖:ReentrantReadWriteLock,其提供了部分互斥的鎖實現,以後的總結會有介紹。