Java併發編程指南(三):線程同步工具

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時,並保護共享資源的訪問:

  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() 方法:
  • acquireUninterruptibly():acquire()方法是當semaphore的內部計數器的值爲0時,阻塞線程直到semaphore被釋放。在阻塞期間,線程可能會被中斷,然後此方法拋出InterruptedException異常。而此版本的acquire方法會忽略線程的中斷而且不會拋出任何異常。
  • tryAcquire():此方法會嘗試獲取semaphore。如果成功,返回true。如果不成功,返回false值,並不會被阻塞和等待semaphore的釋放。接下來是你的任務用返回的值執行正確的行動。
Semaphores的公平性

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個基本元素:
  1. 初始值決定CountDownLatch類需要等待的事件的數量。
  2. await() 方法, 被等待全部事件終結的線程調用。
  3. countDown() 方法,事件在結束執行後調用。
當創建 CountDownLatch 對象時,對象使用構造函數的參數來初始化內部計數器。每次調用 countDown() 方法, CountDownLatch 對象內部計數器減一。當內部計數器達到0時, CountDownLatch 對象喚醒全部使用 await() 方法睡眠的線程們。
不可能重新初始化或者修改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》


發佈了70 篇原創文章 · 獲贊 28 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章