1. 控制併發訪問資源Semaphore:
Semaphore是一個控制訪問多個共享資源的計數器。
當一個線程想要訪問某個共享資源,首先,它必須獲得semaphore。如果semaphore的內部計數器的值大於0,那麼semaphore減少計數器的值並允許訪問共享的資源。計數器的值大於0表示,有可以自由使用的資源,所以線程可以訪問並使用它們。
另一種情況,如果semaphore的計數器的值等於0,那麼semaphore讓線程進入休眠狀態一直到計數器大於0。計數器的值等於0表示全部的共享資源都正被線程們使用,所以此線程想要訪問就必須等到某個資源成爲自由的。
當線程使用完共享資源時,他必須放出semaphore爲了讓其他線程可以訪問共享資源。這個操作會增加semaphore的內部計數器的值。
//1. 創建一個會實現print queue的類名爲 PrintQueue。
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 接口。這個類實現把文檔傳送到打印機的任務。
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()方法。
class MainClient {
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時,並保護共享資源的訪問:
- 首先, 你要調用acquire()方法獲得semaphore。
- 然後, 對共享資源做出必要的操作。
- 最後, 調用release()方法來釋放semaphore。
當你開始10個threads,當你開始10個threads時,那麼第一個獲得semaphore的得到critical section的訪問權。剩下的線程都會被semaphore阻塞直到那個獲得semaphore的線程釋放它。當這情況發生,semaphore在等待的線程中選擇一個並給予它訪問critical section的訪問權。全部的任務都會打印文檔,只是一個接一個的執行。
更多…
Semaphore類有另2個版本的 acquire() 方法:
- acquireUninterruptibly():acquire()方法是當semaphore的內部計數器的值爲0時,阻塞線程直到semaphore被釋放。在阻塞期間,線程可能會被中斷,然後此方法拋出InterruptedException異常。而此版本的acquire方法會忽略線程的中斷而且不會拋出任何異常。
- tryAcquire():此方法會嘗試獲取semaphore。如果成功,返回true。如果不成功,返回false值,並不會被阻塞和等待semaphore的釋放。接下來是你的任務用返回的值執行正確的行動。
fairness的內容是指全java語言的所有類中,那些可以阻塞多個線程並等待同步資源釋放的類(例如,semaphore)。默認情況下是非公平模式。在這個模式中,當同步資源釋放,就會從等待的線程中任意選擇一個獲得資源,但是這種選擇沒有任何標準。而公平模式可以改變這個行爲並強制選擇等待最久時間的線程。
隨着其他類的出現,Semaphore類的構造函數容許第二個參數。這個參數必需是 Boolean 值。如果你給的是 false 值,那麼創建的semaphore就會在非公平模式下運行。如果你不使用這個參數,是跟給false值一樣的結果。如果你給的是true值,那麼你創建的semaphore就會在公平模式下運行。
2. 控制併發訪問多個資源:
semaphore=new Semaphore(3);
Semaphore對象創建的構造方法是使用3作爲參數的。前3個調用acquire() 方法的線程會獲得臨界區的訪問權,其餘的都會被阻塞 。當一個線程結束臨界區的訪問並解放semaphore時,另外的線程纔可能獲得訪問權。
3. 等待多個併發事件完成CountDownLatch:
Java併發API提供這樣的類,它允許1個或者多個線程一直等待,直到一組操作執行完成。 這個類就是CountDownLatch類。它初始一個整數值,此值是線程將要等待的操作數。當某個線程爲了想要執行這些操作而等待時, 它要使用 await()方法。此方法讓線程進入休眠直到操作完成。 當某個操作結束,它使用countDown() 方法來減少CountDownLatch類的內部計數器。當計數器到達0時,這個類會喚醒全部使用await() 方法休眠的線程們。
//1. 創建一個類名爲 Videoconference 並特別實現 Runnable 接口。這個類將實現 video-conference 系統。
class Videoconference implements Runnable {
//2. 聲明 CountDownLatch 對象名爲 controller。
private final CountDownLatch controller;
//3. 實現類的構造函數,初始 CountDownLatch 屬性。Videoconference 類接收將要等待的參與者的量爲參數。
public Videoconference(int number) {
controller = new CountDownLatch(number);
}
//4. 實現 arrive() 方法。每次有參與者到達都會調用此方法。它接收String類型的參數名爲 name。
public void arrive(String name) {
//5. 首先,它輸出某某參數已經到達。
System.out.printf("%s has arrived.", name);
//6. 然後,調用CountDownLatch對象的 countDown() 方法。
controller.countDown();
//7. 最後,使用CountDownLatch對象的 getCount() 方法輸出另一條關於還未確定到達的參與者數。
System.out.printf("VideoConference: Waiting for %d participants.\n", controller.getCount());
}
//8. 實現video-conference 系統的主方法。它是每個Runnable都必須有的 run() 方法。
@Override
public void run() {
//9. 首先,使用 getCount() 方法來輸出這次video conference的參與值的數量信息。
System.out.printf("VideoConference: Initialization: %d participants.\n", controller.getCount());
//10. 然後, 使用 await() 方法來等待全部的參與者。由於此法會拋出 InterruptedException 異常,所以要包含處理代碼。
try {
controller.await();
//11. 最後,輸出信息表明全部參與者已經到達。
System.out.printf("VideoConference: All the participants have come\n");
System.out.printf("VideoConference: Let's start...\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//12. 創建 Participant 類並實現 Runnable 接口。這個類表示每個video conference的參與者。
class Participant implements Runnable {
//13. 聲明一個私有 Videoconference 屬性名爲 conference.
private Videoconference conference;
//14. 聲明一個私有 String 屬性名爲 name。
private String name;
//15. 實現類的構造函數,初始化那2個屬性。
public Participant(Videoconference conference, String name) {
this.conference = conference;
this.name = name;
}
//16. 實現參與者的run() 方法。
@Override
public void run() {
//17. 首先,讓線程隨機休眠一段時間。
long duration = (long) (Math.random() * 10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
//18. 然後,使用Videoconference 對象的arrive() 方法來表明參與者的到達。
conference.arrive(name);
}
}
//19. 最後,實現例子的 main 類通過創建一個名爲 Main 的類併爲其添加 main() 方法。
class Client1 {
public static void main(String[] args) {
//20. 創建 Videoconference 對象名爲 conference,將等待10個參與者。
Videoconference conference = new Videoconference(10);
//21. 創建 Thread 來運行這個 Videoconference 對象並開始運行。
Thread threadConference = new Thread(conference);
threadConference.start();
//22. 創建 10個 Participant 對象,爲每個對象各創建一個 Thread 對象來運行他們,開始運行全部的線程。
for (int i = 0; i < 10; i++) {
Participant p = new Participant(conference, "Participant " + i);
Thread t = new Thread(p);
t.start();
}
}
}
CountDownLatch類有3個基本元素:- 初始值決定CountDownLatch類需要等待的事件的數量。
- await() 方法, 被等待全部事件終結的線程調用。
- countDown() 方法,事件在結束執行後調用。
不可能重新初始化或者修改CountDownLatch對象的內部計數器的值。一旦計數器的值初始後,唯一可以修改它的方法就是之前用的 countDown() 方法。當計數器到達0時, 全部調用 await() 方法會立刻返回,接下來任何countDown() 方法的調用都將不會造成任何影響。
此方法與其他同步方法有這些不同:
CountDownLatch 機制不是用來保護共享資源或者臨界區。它是用來同步一個或者多個執行多個任務的線程。它只能使用一次。像之前解說的,一旦CountDownLatch的計數器到達0,任何對它的方法的調用都是無效的。如果你想再次同步,你必須創建新的對象。
CountDownLatch 類有另一種版本的 await() 方法,它是:
await(long time, TimeUnit unit): 此方法會休眠直到被中斷; CountDownLatch 內部計數器到達0或者特定的時間過去了。
4.在同一個點同步任務CyclicBarrier:
Java 併發 API 提供了可以允許多個線程在在一個確定點進行同步。它是 CyclicBarrier 類。此類與在此章節的等待多個併發事件完成指南中的 CountDownLatch 類相似,但是它有一些特殊性讓它成爲更強大的類。
CyclicBarrier 類有一個整數初始值,此值表示將在同一點同步的線程數量。當其中一個線程到達確定點,它會調用await() 方法來等待其他線程。當線程調用這個方法,CyclicBarrier阻塞線程進入休眠直到其他線程到達。當最後一個線程調用CyclicBarrier 類的await() 方法,它喚醒所有等待的線程並繼續執行它們的任務。
CyclicBarrier 類有個有趣的優勢是,你可以傳遞一個外加的 Runnable 對象作爲初始參數,並且當全部線程都到達同一個點時,CyclicBarrier類 會把這個對象當做線程來執行。此特點讓這個類在使用 divide 和 conquer 編程技術時,可以充分發揮任務的並行性。
//1. 我們從實現2個輔助類開始。首先,創建一個類名爲 MatrixMock。此類隨機生成一個在1-10之間的 數字矩陣,我們將從中查找數字。
class MatrixMock {
//2. 聲明私有 int matrix,名爲 data。
private int data[][];
//3. 實現類的構造函數。此構造函數將接收矩陣的行數,行的長度,和我們將要查找的數字作爲參數。3個參數全部int 類型。
public MatrixMock(int size, int length, int number) {
//4. 初始化構造函數將使用的變量和對象。
int counter = 0;
data = new int[size][length];
Random random = new Random();
//5. 用隨機數字填充矩陣。每生成一個數字就與要查找的數字對比,如果相等,就增加counter值。
for (int i = 0; i < size; i++) {
for (int j = 0; j < length; j++) {
data[i][j] = random.nextInt(10);
if (data[i][j] == number) {
counter++;
}
}
}
//6. 最後,在操控臺打印一條信息,表示查找的數字在生成的矩陣裏的出現次數。此信息是用來檢查線程們獲得的正確結果的。
System.out.printf("Mock: There are %d ocurrences of number %d in generated data.\n", counter, number);
}
//7. 實現 getRow() 方法。此方法接收一個 int爲參數,是矩陣的行數。返回行數如果存在,否則返回null。
public int[] getRow(int row) {
if ((row >= 0) && (row < data.length)) {
return data[row];
}
return null;
}
}
//8. 現在,實現一個類名爲 Results。此類會在array內保存被查找的數字在矩陣的每行裏出現的次數。
class Results {
//9. 聲明私有 int array 名爲 data。
private int data[];
//10. 實現類的構造函數。此構造函數接收一個表明array元素量的整數作爲參數。
public Results(int size) {
data = new int[size];
}
//11. 實現 setData() 方法。此方法接收array的某個位置和一個值作爲參數,然後把array的那個位置設定爲那個值。
public void setData(int position, int value) {
data[position] = value;
}
//12. 實現 getData() 方法。此方法返回結果 array。
public int[] getData() {
return data;
}
}
//13. 現在你有了輔助類,是時候來實現線程了。首先,實現 Searcher 類。這個類會在隨機數字的矩陣中的特定的行裏查找數字。
// 創建一個類名爲Searcher 並一定實現 Runnable 接口.
class Searcher implements Runnable {
//14. 聲明2個私有int屬性名爲 firstRow 和 lastRow。這2個屬性是用來確定將要用的子集的行。
private int firstRow;
private int lastRow;
//15. 聲明一個私有 MatrixMock 屬性,名爲 mock。
private MatrixMock mock;
//16. 聲明一個私有 Results 屬性,名爲 results。
private Results results;
//17. 聲明一個私有 int 屬性名爲 number,用來儲存我們要查找的數字。
private int number;
//18. 聲明一個 CyclicBarrier 對象,名爲 barrier。
private final CyclicBarrier barrier;
//19. 實現類的構造函數,並初始化之前聲明的全部屬性。
public Searcher(int firstRow, int lastRow, MatrixMock mock, Results results, int number, CyclicBarrier barrier) {
this.firstRow = firstRow;
this.lastRow = lastRow;
this.mock = mock;
this.results = results;
this.number = number;
this.barrier = barrier;
}
//20. 實現 run() 方法,用來查找數字。它使用內部變量,名爲counter,用來儲存數字在每行出現的次數。
@Override
public void run() {
int counter;
//21. 在操控臺打印一條信息表明被分配到這個對象的行。
System.out.printf("%s: Processing lines from %d to %d.\n", Thread.currentThread().getName(), firstRow, lastRow);
//22. 處理分配給這個線程的全部行。對於每行,記錄正在查找的數字出現的次數,並在相對於的 Results 對象中保存此數據。
for (int i = firstRow; i < lastRow; i++) {
int row[] = mock.getRow(i);
counter = 0;
for (int j = 0; j < row.length; j++) {
if (row[j] == number) {
counter++;
}
}
results.setData(i, counter);
}
//23. 打印信息到操控臺表明此對象已經結束搜索。
System.out.printf("%s: Lines processed.\n", Thread.currentThread().getName());
//24. 調用 CyclicBarrier 對象的 await() 方法 ,由於可能拋出的異常,要加入處理 InterruptedException and BrokenBarrierException 異常的必需代碼。
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
//25. 現在,實現一個類來計算數字在這個矩陣裏出現的總數。它使用儲存了矩陣中每行裏數字出現次數的 Results 對象來進行運算。創建一個類,名爲 Grouper 並一定實現 Runnable 接口.
class Grouper implements Runnable {
//26. 聲明一個私有 Results 屬性,名爲 results。
private Results results;
//27. 實現類的構造函數,並初始化 Results 屬性。
public Grouper(Results results) {
this.results = results;
}
//28.實現 run() 方法,用來計算結果array裏數字出現次數的總和。
@Override
public void run() {
//29. 聲明一個 int 變量並寫在操控臺寫一條信息表明開始處理了。
int finalResult = 0;
System.out.printf("Grouper: Processing results...\n");
//30. 使用 results 對象的 getData() 方法來獲得每行數字出現的次數。然後,處理array的全部元素,把每個元素的值加給 finalResult 變量。
int data[] = results.getData();
for (int number : data) {
finalResult += number;
}
//31. 在操控臺打印結果。
System.out.printf("Grouper: Total result: %d.\n", finalResult);
}
}
//32. 最後, 實現例子的 main 類,通過創建一個類,名爲 Main 併爲其添加 main() 方法。
class Main2 {
public static void main(String[] args) {
//33. 聲明並初始5個常熟來儲存應用的參數。
final int ROWS = 10000;
final int NUMBERS = 1000;
final int SEARCH = 5;
final int PARTICIPANTS = 5;
final int LINES_PARTICIPANT = 2000;
//34. Create a MatrixMock 對象,名爲 mock. 它將有 10,000 行,每行1000個元素。現在,你要查找的數字是5。
MatrixMock mock = new MatrixMock(ROWS, NUMBERS, SEARCH);
//35. 創建 Results 對象,名爲 results。它將有 10,000 元素。
Results results = new Results(ROWS);
//36. 創建 Grouper 對象,名爲 grouper。
Grouper grouper = new Grouper(results);
//37. 創建 CyclicBarrier 對象,名爲 barrier。此對象會等待5個線程。當此線程結束後,它會執行前面創建的 Grouper 對象。
CyclicBarrier barrier = new CyclicBarrier(PARTICIPANTS, grouper);
//38. 創建5個 Searcher 對象,5個執行他們的線程,並開始這5個線程。
Searcher searchers[] = new Searcher[PARTICIPANTS];
for (int i = 0; i < PARTICIPANTS; i++) {
searchers[i] = new Searcher(i * LINES_PARTICIPANT, (i * LINES_PARTICIPANT) + LINES_PARTICIPANT, mock, results, 5, barrier);
Thread thread = new Thread(searchers[i]);
thread.start();
}
System.out.printf("Main: The main thread has finished.\n");
}
}
例子中解決的問題比較簡單。我們有一個很大的隨機的整數矩陣,然後你想知道這矩陣裏面某個數字出現的次數。爲了更好的執行,我們使用了 divide 和 conquer 技術。我們 divide 矩陣成5個子集,然後在每個子集裏使用一個線程來查找數字。這些線程是 Searcher 類的對象。我們使用 CyclicBarrier 對象來同步5個線程的完成,並執行 Grouper 任務處理個別結果,最後計算最終結果。
如我們之前提到的,CyclicBarrier 類有一個內部計數器控制到達同步點的線程數量。每次線程到達同步點,它調用 await() 方法告知 CyclicBarrier 對象到達同步點了。CyclicBarrier 把線程放入睡眠狀態直到全部的線程都到達他們的同步點。
當全部的線程都到達他們的同步點,CyclicBarrier 對象叫醒全部正在 await() 方法中等待的線程們,然後,選擇性的,爲CyclicBarrier的構造函數 傳遞的 Runnable 對象(例子裏,是 Grouper 對象)創建新的線程執行外加任務。
CyclicBarrier 類有另一個版本的 await() 方法:
await(long time, TimeUnit unit): 線程會一直休眠直到被中斷;內部計數器到達0,或者特定的時間過去了。
此類也提供了 getNumberWaiting() 方法,返回被 await() 方法阻塞的線程數,還有 getParties() 方法,返回將與CyclicBarrier同步的任務數。
重置 CyclicBarrier 對象
CyclicBarrier 類與CountDownLatch有一些共同點,但是也有一些不同。最主要的不同是,CyclicBarrier對象可以重置到它的初始狀態,重新分配新的值給內部計數器,即使它已經被初始過了。
可以使用 CyclicBarrier的reset() 方法來進行重置操作。當這個方法被調用後,全部的正在await() 方法裏等待的線程接收到一個 BrokenBarrierException 異常。此異常在例子中已經用打印stack trace處理了,但是在一個更復制的應用,它可以執行一些其他操作,例如重新開始執行或者在中斷點恢復操作。
破壞 CyclicBarrier 對象
CyclicBarrier 對象可能處於一個特殊的狀態,稱爲 broken。當多個線程正在 await() 方法中等待時,其中一個被中斷了,此線程會收到 InterruptedException 異常,但是其他正在等待的線程將收到 BrokenBarrierException 異常,並且 CyclicBarrier 會被置於broken 狀態中。
CyclicBarrier 類提供了isBroken() 方法,如果對象在 broken 狀態,返回true,否則返回false。
5. 運行階段性併發任務Phaser:
Java 併發 API 提供的一個非常複雜且強大的功能是,能夠使用Phaser類運行階段性的併發任務。當某些併發任務是分成多個步驟來執行時,那麼此機制是非常有用的。Phaser類提供的機制是在每個步驟的結尾同步線程,所以除非全部線程完成第一個步驟,否則線程不能開始進行第二步。
相對於其他同步應用,我們必須初始化Phaser類與這次同步操作有關的任務數,我們可以通過增加或者減少來不斷的改變這個數。
6. 控制併發階段性任務的改變:
Phaser 類提供每次phaser改變階段都會執行的方法。它是 onAdvance() 方法。它接收2個參數:當前階段數和註冊的參與者數;它返回 Boolean 值,如果返回false, phaser繼續執行;如果返回true,即phaser結束運行並進入 termination 狀態。
如果註冊參與者爲0,此方法的默認的實現值爲真,要不然就是false。如果你擴展Phaser類並覆蓋此方法,那麼你可以修改它的行爲。通常,當你要從一個phase到另一個,來執行一些行動時,你會對這麼做感興趣的。
7. 在併發任務間交換數據Exchanger:
Java 併發 API 提供了一種允許2個併發任務間相互交換數據的同步應用。更具體的說,Exchanger 類允許在2個線程間定義同步點,當2個線程到達這個點,他們相互交換數據,使用第一個線程的數據變成第二個的,然後第二個線程的數據變成第一個的。
參考資料:《Java 7 Concurrency Cookbook》
《Java 9 Concurrency Cookbook Second Edition》