在尚未出現java.util.concurrent套件以前,多執行緒的程式都需要自己設計wait(), notify(), notifyAll()等程序,一個設計不良還可能造成死結(deadlock)的狀況。
在JDK5以後,就可以使用現成的java.util.concurrent套件來更便利的撰寫多執行緒相關程式。
Lock介面常用的實作類別為ReentrantLock,可以透過呼叫lock()方法讓執行緒取得鎖,並以呼叫unlock()來釋放鎖:
private Lock lock = new ReentrantLock();
private Object[] array;
private int next;
public void add(E e){
lock.lock();
try{
if(next == array.length){
array = Arrays.copyOf(array, array.length * 2);
}
array[next++] = e;
}finally{
lock.unlock();
}
}
除了基本的lock()以及unlock(),還有tryLock()方法,傳回boolean,true表示可以取得鎖,有了這個方法,原先產生死結的程式就可以透過呼叫tryLock()來解開死結。
由於寫入動作是產生多執行緒資料混亂的罪魁禍首,若讓其他讀取的動作也都進行同樣的鎖定程序,會導致效能低下,這時就可以用ReadWriteLock來增進程式的效能。
其原理為,讀取動作使用readLock()來進行鎖定,寫入動作使用writeLock()來進行鎖定。
當有執行緒企圖取得readLock時,只要物件的writeLock沒有被取走,那就可以被拿走;
當有執行緒企圖取得writeLock時,條件不只是writeLock沒有被取走,連readLock也沒有被取走時,才能允許執行緒取得writeLock。
上述的ReentrantReadWriteLock實作是比較嚴謹的執行緒鎖定管控了,因為只要有任何執行緒再進行寫入動作,也不能夠讀取,稱為悲觀讀取(Pessimistic Reading)。假若我們的程式不需要這麼嚴格的管控呢?縱使被讀取的物件狀態會和真實狀態不一樣,但我們知道誤差不會太大抑或是有誤差我們無所謂,那就可以改用StampLock的實作類別。
long stamp = stampLock.tryOptimisticRead();
int num = array[i];
if(!stampLock.validate(stamp)){
stamp = stampLock.readLock();
try{
num = array[i];
}finally{
stampLock.unlockRead(stamp);
}
}
return num;
Condition的作用就是可以擁有不只一個物件的執行緒等待集,原生物件的wait(), notify()會共用該物件的等待集,所以不管執行緒原本是預備執行什麼動作,只要被叫進等待集,就是統一等待叫號;運用Condition物件的話,就可以依據需求將執行緒放進不同的等待集,當當前執行緒完成動作要教下一個來執行的時候,可以精準呼叫出該流程等待集中的執行緒,減少整體程式呼叫到無用執行緒的效能。
private Lock lock = new ReentrantLock();
private Condition aCondition = lock.newCondition();
private Condition bCondition = lock.newCondition();
public void aMethod(){
lock.lock()
try{
isAOk();
doSomething();
bCondition.signal(); //相當於Object的notify()
}finally{
lock.unlock()
}
}
public void isAOk(){
if( someCondition() ){
aCondition.await(); //相當於Object的wait()
}
}
public void bMethod(){
lock.lock()
try{
isBOk();
doSomething();
aCondition.signal();
}finally{
lock.unlock()
}
}
public void isBOk(){
if( someCondition() ){
bCondition.await();
}
}