1. 程式人生 > >執行緒同步工具(一)控制併發訪問資源

執行緒同步工具(一)控制併發訪問資源

宣告:本文是《 Java 7 Concurrency Cookbook 》的第三章, 作者: Javier Fernández González 譯者:鄭玉婷    

控制併發訪問資源

這個指南,你將學習怎樣使用Java語言提供的Semaphore機制。Semaphore是一個控制訪問多個共享資源的計數器。

Semaphore的內容是由Edsger Dijkstra引入並在 THEOS作業系統上第一次使用。

當一個執行緒想要訪問某個共享資源,首先,它必須獲得semaphore。如果semaphore的內部計數器的值大於0,那麼semaphore減少計數器的值並允許訪問共享的資源。計數器的值大於0表示,有可以自由使用的資源,所以執行緒可以訪問並使用它們。

另一種情況,如果semaphore的計數器的值等於0,那麼semaphore讓執行緒進入休眠狀態一直到計數器大於0。計數器的值等於0表示全部的共享資源都正被執行緒們使用,所以此執行緒想要訪問就必須等到某個資源成為自由的。

當執行緒使用完共享資源時,他必須放出semaphore為了讓其他執行緒可以訪問共享資源。這個操作會增加semaphore的內部計數器的值。

在這個指南里,你將學習如何使用Semaphore類來實現一種比較特殊的semaphores種類,稱為binary semaphores。這個semaphores種類保護訪問共享資源的獨特性,所以semaphore的內部計數器的值只能是1或者0。為了展示如何使用它,你將要實現一個PrintQueue類來讓併發任務列印它們的任務。這個PrintQueue類會受到binary semaphore的保護,所以每次只能有一個執行緒可以列印。

準備

指南中的例子是使用Eclipse IDE 來實現的。如果你使用Eclipse 或者其他的IDE,例如NetBeans, 開啟並建立一個新的java任務。

怎麼做呢

按照這些步驟來實現下面的例子:

//1.   建立一個會實現print queue的類名為 PrintQueue。
public class PrintQueue {

//2.   宣告一個物件為Semaphore,稱它為semaphore。
private final Semaphore semaphore;

//3.   實現類的建構函式並初始能保護print quere的訪問的semaphore物件的值。
public PrintQueue(){
	semaphore=new Semaphore(1);
}

//4.   實現Implement the printJob()方法,此方法可以模擬列印文件,並接收document物件作為引數。
public void printJob (Object document){

//5.   在這方法內,首先,你必須呼叫acquire()方法獲得demaphore。這個方法會丟擲 InterruptedException異常,使用必須包含處理這個異常的程式碼。
try {
	semaphore.acquire();

//6.   然後,實現能隨機等待一段時間的模擬列印文件的行。
long duration=(long)(Math.random()*10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),duration);
Thread.sleep(duration);

//7.	最後,釋放semaphore通過呼叫semaphore的relaser()方法。
} catch (InterruptedException e) {
	e.printStackTrace();
} finally {
	semaphore.release();
}

//8.   建立一個名為Job的類並一定實現Runnable 介面。這個類實現把文件傳送到印表機的任務。
public class Job implements Runnable {

//9.   宣告一個物件為PrintQueue,名為printQueue。
private PrintQueue printQueue;

//10. 實現類的建構函式,初始化這個類裡的PrintQueue物件。
public Job(PrintQueue printQueue){
	this.printQueue=printQueue;
}

//11. 實現方法run()。
@Override
public void run() {

//12. 首先, 此方法寫資訊到操控臺表明任務已經開始執行了。
System.out.printf("%s: Going to print a job\n",Thread. currentThread().getName());

//13. 然後,呼叫PrintQueue 物件的printJob()方法。
printQueue.printJob(new Object());

//14. 最後, 此方法寫資訊到操控臺表明它已經結束運行了。
System.out.printf("%s: The document has been printed\n",Thread.currentThread().getName());
}

//15. 實現例子的main類,建立名為 Main的類並實現main()方法。
public class Main {
	public static void main (String args[]){

//16. 建立PrintQueue物件名為printQueue。
PrintQueue printQueue=new PrintQueue();

//17. 建立10個threads。每個執行緒會執行一個傳送文件到print queue的Job物件。
Thread thread[]=new Thread[10];
for (int i=0; i<10; i++){
thread[i]=new Thread(new Job(printQueue),"Thread"+i);
}

//18. 最後,開始這10個執行緒們。
for (int i=0; i<10; i++){
thread[i].start();
}

它是怎麼工作的…

這個例子的關鍵是PrintQueue類的printJob()方法。此方法展示了3個你必須遵守的步驟當你使用semaphore來實現critical section時,並保護共享資源的訪問:

1. 首先, 你要呼叫acquire()方法獲得semaphore。
2. 然後, 對共享資源做出必要的操作。
3. 最後, 呼叫release()方法來釋放semaphore。

另一個重點是PrintQueue類的構造方法和初始化Semaphore物件。你傳遞值1作為此構造方法的引數,那麼你就建立了一個binary semaphore。初始值為1,就保護了訪問一個共享資源,在例子中是print queue。

當你開始10個threads,當你開始10個threads時,那麼第一個獲得semaphore的得到critical section的訪問權。剩下的執行緒都會被semaphore阻塞直到那個獲得semaphore的執行緒釋放它。當這情況發生,semaphore在等待的執行緒中選擇一個並給予它訪問critical section的訪問權。全部的任務都會列印文件,只是一個接一個的執行。

更多…

Semaphore類有另2個版本的 acquire() 方法:

  1. acquireUninterruptibly():acquire()方法是當semaphore的內部計數器的值為0時,阻塞執行緒直到semaphore被釋放。在阻塞期間,執行緒可能會被中斷,然後此方法丟擲InterruptedException異常。而此版本的acquire方法會忽略執行緒的中斷而且不會丟擲任何異常。
  2. tryAcquire():此方法會嘗試獲取semaphore。如果成功,返回true。如果不成功,返回false值,並不會被阻塞和等待semaphore的釋放。接下來是你的任務用返回的值執行正確的行動。

Semaphores的公平性

fairness的內容是指全java語言的所有類中,那些可以阻塞多個執行緒並等待同步資源釋放的類(例如,semaphore)。預設情況下是非公平模式。在這個模式中,當同步資源釋放,就會從等待的執行緒中任意選擇一個獲得資源,但是這種選擇沒有任何標準。而公平模式可以改變這個行為並強制選擇等待最久時間的執行緒。

隨著其他類的出現,Semaphore類的建構函式容許第二個引數。這個引數必需是 Boolean 值。如果你給的是 false 值,那麼建立的semaphore就會在非公平模式下執行。如果你不使用這個引數,是跟給false值一樣的結果。如果你給的是true值,那麼你建立的semaphore就會在公平模式下執行。

參見