我們在線程同步機制(一)--Synchronized和Lock簡要介紹中學習了同步和臨界區的概念,並且討論了多個併發任務共享一個資源時的同步情況。訪問共享資源的代碼塊叫臨界區。
我們在線程同步機制(一)--Synchronized和Lock簡要介紹中學習了一下內容:
synchronized關鍵字
Lock接口及其實現類,如ReentrantLock,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock
本章我們將學習如何使用更高級的同步機制來實現多線程間的同步。
信號量(Semaphore):一種計數器,用來保護一個或多個共享資源的訪問。它是併發編程的一個基礎工具。
CountDownLatch:是Java語言提供的同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許線程一直等待
CyclicBarrier:Java語言提供的同步輔助類,允許多個線程在某個集合點處進行相互等待
Phaser:它把併發任務分多個階段運行,在開始下一個階段之前,當前階段中所有的線程都必須執行完成。
Exchanger:它提供了兩個線程之間的數據交換點。
在應用程序中,任何時候都可以使用Semaphore來保護臨界區,因爲它是一個基礎的同步機制。
1、資源的併發訪問控制。
Java語言提供了信號量(Semaphore)機制。信號量是一種計數器,用來保護一個或多個共享資源的訪問。如果線程要訪問一個共享資源,它必須要獲得信號量。如果信號量的內部計數器大於0,信號量將減1,然後允許訪問這個共享資源。計數器大於0意味着有可以使用的資源,因此線程被允許使用其中一個資源。否則,如果信號的計數器等於0,信號量就會把線程置入休眠直到計數器大於0.計數器等於0的時候意味着所有的共享資源已經被其他線程使用了,所以需要訪問這個共享資源的線程必須等待。
當線程使用完某個共享資源時,信號量必須釋放,以便其他線程能夠訪問共享資源。釋放操作將使信號量內部計數器增加1.
使用信號量實現臨界區必須遵循3個步驟,從而保護對資源的訪問。
(1).必須通過acquire()方法獲得信號量
(2).使用共享資源執行必要的操作
(3).必須通過release()方法釋放信號量
2、等待多個併發事件的完成
Java併發API提供了CountDownLatch類。在完成一組正在其他線程中執行的操作之前,它允許線程一直等待。這個類使用一個整數進行初始化,這個整數是線程要等待完成的操作的數目。當一個線程要等待某些操作先完成時,需要調用await()方法。當一個線程操作完成後,它將調用countDown()方法使內部計數器減1.當計數器變成0的時候,CountDownLatch類將喚醒所有調用await()而進入休眠的線程
CountDownLatch類有三個基本元素:
一個初始值,即定義必須等待的先行完成的操作數據;
await()方法,需要等待其他事件先完成的線程調用
countDown()方法,每個被等待的事件在完成操作後調用,使內部計數器減1.當內部計數器到達0時,countDownLatch對象將喚醒所有在await()方法上等待的線程
3、併發階段任務的運行
Java併發API還提供了Phaser,它允許執行併發多階段任務。當我們有併發任務並且需要分解成幾步執行時,這種機制就非常實用。Phaser類機制是在每一步結束的位置對線程進行同步,當所有線程都執行完了這一步,才允許執行下一步。Phaser類提供了onAdvance()方法,它在phaser階段改變的時候自動執行。下面我們將通過一個文件查找示例來演示。
package org.test.concurrency.phaser; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.Phaser; import java.util.concurrent.TimeUnit; /** * <p> * Title: FileSearch.java * </p> * <p> * Description: * </p> * <p> * Copyright: Copyright (c) 2016年6月11日 * </p> * <p> * Company: * </p> * Created on 2016年6月11日 上午8:32:06 * * @author kucs FileSearch * * Description:創建文件查找類,實現runnable結構。 * 他將在一個文件夾及其子文件夾中查找過去24小時內修改過的指定擴展名的文件。 * ----------------------------------------------------- * */ public class FileSearch implements Runnable { // 存儲要查找額文件夾 private String initPath; // 存儲要查找的文件的擴展名 private String end; // 存儲查找到的文件的完整路徑 private List<String> results; // 聲明一個Phaser私有變量,用來控制任務不同階段的同步 private Phaser phaser; public FileSearch(String initPath, String end, Phaser phaser) { this.initPath = initPath; this.end = end; this.results = new ArrayList<>(); this.phaser = phaser; } /** * 實現輔助方法,他們將用於run方法中 * */ /** * directoryProcess,用於處理所有文件和文件夾 * * @param file */ public void directoryProcess(File file) { File[] list = file.listFiles(); if (list != null && list.length > 0) { for (File f : list) { if (f.isDirectory()) { directoryProcess(f); } else { fileProcess(f); } } } } /** 用於查找這個傳入的文件的擴展名是不是我們指定的,如果是,文件的絕對路徑將被加入結果集中 */ private void fileProcess(File f) { // TODO Auto-generated method stub if (f.getName().endsWith(end)) { results.add(f.getAbsolutePath()); } } /** 對第一個階段查找到的文件列表進行過濾,將不是24小時修改過的文件刪除。 */ public void filterResults() { List<String> newResult = new ArrayList<>(); long actualDate = new Date().getTime(); for (String strFilePath : results) { File file = new File(strFilePath); long fileDate = file.lastModified(); if (actualDate - fileDate < TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS)) { newResult.add(strFilePath); } } results = newResult; } /** 將在第一個階段和第二個階段結束的時候用來檢查結果集是不是空的 */ private boolean checkResults() { if (results.isEmpty()) { System.out.printf("%s: Phaser %d: 0 results.\n", Thread.currentThread().getName(), phaser.getPhase()); System.out.printf("%s: Phaser %d: End.\n", Thread.currentThread().getName(), phaser.getPhase()); phaser.arriveAndDeregister(); return false; } else { System.out.printf("%s: Phaser %d: results.\n", Thread.currentThread().getName(), phaser.getPhase(), results.size()); phaser.arriveAndAwaitAdvance(); return true; } } /** 將結果集元素打印到控制檯 */ private void showInfo() { for (String strFilePath : results) { File file = new File(strFilePath); System.out.printf("%s: %s\n", Thread.currentThread().getName(), file.getAbsolutePath()); } phaser.arriveAndAwaitAdvance(); } @Override public void run() { // TODO 調用Phaser對象的arriveAndAwaitAdvance()方法,使查找工作在所有線程都被創建之後再開始。 phaser.arriveAndAwaitAdvance(); System.out.printf("%s: Starting.\n", Thread.currentThread().getName()); File file = new File(initPath); if (file.isDirectory()) { directoryProcess(file); } // 檢查結果集是不是空的,如果是空的,結束對應線程,並使用return返回 if (!checkResults()) { return; } // 對結果集進行過濾 filterResults(); // 檢查結果集是不是空的,如果是空的,結束對應線程,並使用return返回 if (!checkResults()) { return; } //將結果打印到控制檯,撤銷線程的註冊,然後將線程完成信息打印到控制檯 showInfo(); phaser.arriveAndDeregister(); System.out.printf("%s: Work completed.\n",Thread.currentThread().getName()); } } package org.test.concurrency.phaser; import java.util.concurrent.Phaser; public class PhaserMain { public static void main(String[] args) { // TODO 創建一個Phaser對象,並指定參與階段同步的線程是3個 Phaser phaser = new Phaser(3); //定義要查到的文件目錄,後綴 FileSearch system = new FileSearch("C:\\Windows", "log", phaser); FileSearch apps = new FileSearch("C:\\Program Files", "log", phaser); FileSearch document = new FileSearch("C:\\Documents And Settings", "log", phaser); Thread systemThread = new Thread(system, "System"); systemThread.start(); Thread appsThread = new Thread(apps, "Apps"); appsThread.start(); Thread documentThread = new Thread(document, "Document"); documentThread.start(); try { systemThread.join(); appsThread.join(); documentThread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("Terminated: "+phaser.isTerminated()); } }
運行結果:
Phaser構造器傳入了參與階段同步的線程的個數。在這個例子中,Phaser有三個參與線程。這個數字通知Phaser在喚醒所有休眠線程之前,必須執行arriveAndAwaitAdvance()方法的線程數。在Phaser創建之後,我們使用三個不同的文件查找對象創建了三個線程並啓動他們。
Phaser類提供的方法的簡要介紹
arrive():這個方法通知phaser對象一個參與者已經完成了當前階段,但是它不應該等待其他參與者都完成當前階段。它不會與其他線程同步。
awaitAdvance(int phase):如果傳入的階段參數與當前階段一直,這個方法會將當前線程置入休眠,知道這個階段的所有 參與者都運行完成。如果傳入的階段參數與當前階段不一致,這個方法就立即返回。
awaitAdvanceInterruptibly(int phaser):這個方法跟awaitAdvance(int phase)一個樣,不同之處是,如果在這個方法中休眠的線程被中斷,它將拋出InterruptedException。
將參與者註冊到Phaser中。
創建一個Phaser對象時,需要指出有多少參與者,Phaser類提供了兩種方法增加註冊者的數量。
register():這個方法將一個新的參與者註冊到Phaser中,這個新的參與者將被當成執行本階段的線程。
bulkRegister(int phaser):這個方法將指定數目的參與者註冊到Phaser
Phaser提供了一種方法減少註冊者的數目,即arriveAndDeregister()。它通知phaser對象對應的線程已經完成當前階段,並且它不會參與到下一個階段。
4、併發任務之間的數據交換Exchanger。它允許在併發任務之間的數據交換,具體來說,Exchanger類允許兩個線程定義同步點。當兩個線程都達到同步點時,他們交換數據結構。