Java常用併發包

一、Fork/Join


Java7提供了Fork/Join用於並行執行任務的框架, 可以把一個大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。

如果一個應用能被分解成多個子任務,並且組合多個子任務的結果就能夠獲得最終的答案,那麼這個應用就適合用 Fork/Join 模式來解決,對開發來說也不再需要處理各種並行相關事務,例如同步、通信、死鎖等問題,需要做的就是拆分任務並組合每個子任務的中間結果。

1.工作竊取

JDK1.7引入的Fork/Join框架就是基於工作竊取算法,是指某個線程從其他隊列裏竊取任務來執行。工作竊取算法的優點是充分利用線程進行並行計算,並減少了線程間的競爭,其缺點是在某些情況下還是存在競爭,比如雙端隊列裏只有一個任務時。並且消耗了更多的系統資源,比如創建多個線程和多個雙端隊列。

一般會使用雙端隊列,比如AB線程分別處理AB兩個任務隊列,當有一個線程執行完一個任務隊列時,會去竊取另一個未完成的隊列任務,而被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。

source: Fork/Join框架介紹

2.類關係和API

  • RecursiveAction不需要返回值。
  • RecursiveTask通過泛型參數設置計算的返回值類型。
  • ForkJoinPool提供了一系列的submit方法,計算任務。ForkJoinPool默認的線程數通Runtime.availableProcessors()獲得,因爲在計算密集型的任務中,獲得多於處理性核心數的線程並不能獲得更多性能提升。

在RecursiveTask類中最重要的是實現compute()接口,此處定義了計算和拆分方法。
fork() - 每個子任務在調用fork方法時,又會進入compute方法,如果還要拆分則繼續遞歸調用。
join() - 使用join方法會等待子任務執行完並得到其結果。
invokeAll() - 可變參數,觸發所有輸入的Task來計算。

假設在計算一個求和算法時,就可以採用Fork/Join方法進行並行計算。

public class Calculator extends RecursiveTask<Integer> {  
 
    private static final int THRESHOLD = 100;  
    private int start;  
    private int end;  
  
    public Calculator(int start, int end) {  
        this.start = start;  
        this.end = end;  
    }  
  
    @Override  
    protected Integer compute() {  
        int sum = 0;  
        if((start - end) < THRESHOLD){  
            for(int i = start; i< end;i++){  
                sum += i;  
            }  
        }else{  
            int middle = (start + end) /2;  
            Calculator left = new Calculator(start, middle);  
            Calculator right = new Calculator(middle + 1, end);  
            left.fork();  
            right.fork();  
            sum = left.join() + right.join();  
        }  
        return sum;  
    }  

}  

source:Java 7 Fork/Join 並行計算框架概覽

二、CountDownLatch ( 重點! )


CountDownLatch,又名發令槍。這個類能夠使一個線程等待其他線程完成各自的工作後再執行。例如,應用程序的主線程希望在負責啓動框架服務的線程已經啓動所有的框架服務之後再執行。

1.API及用法

CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量,每當一個線程完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務。

  • 設定初始值,即構造函數
//參數count爲計數值
public CountDownLatch(int count) {  };  
  • 主線程調用await等待
//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException { };   
//和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException{};  
  • 任務線程執行完計數
//將count值減1
public void countDown() { };  

2.使用場景

  • 實現最大的並行性:有時想同時啓動多個線程,實現最大程度的並行性。例如測試一個單例類,如果我們創建一個初始計數爲1的CountDownLatch,並讓所有線程都在這個鎖上等待,那麼我們可以很輕鬆地完成測試。我們只需調用 一次countDown()方法就可以讓所有的等待線程同時恢復執行。
  • 開始執行前等待n個線程完成各自任務(應用最廣):例如應用程序啓動類要確保在處理用戶請求前,所有N個外部系統已經啓動和運行了。
  • 死鎖檢測:一個非常方便的使用場景是,你可以使用n個線程訪問共享資源,在每次測試階段的線程數目是不同的,並嘗試產生死鎖。

三、CyclicBarrier


CyclicBarrier——迴環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行,叫做迴環是因爲當所有等待線程都被釋放以後,CyclicBarrier可以被重用。

//參數parties指讓多少個線程或者任務等待至barrier狀態
public CyclicBarrier(int parties) {}
//參數barrierAction爲當這些線程都達到barrier狀態時會執行的內容
public CyclicBarrier(int parties, Runnable barrierAction) {}

CyclicBarrier中最重要的方法就是await方法,有重載方法但是一般採用:

//用來掛起當前線程,直至所有線程都到達barrier狀態再同時執行後續任務
public int await() throws InterruptedException, BrokenBarrierException { };

處理同一個動作有多個線程同時執行的場景,用一個柵欄卡在同一個狀態,只有當所有線程抵達該狀態了,纔可以全部放行。

public class TestCyclicBarrier {
    public static void main(String[] args) {
        //1.得到一個CyclicBarrier實例
        CyclicBarrier cb = new CyclicBarrier(4);
        new Thread(new Fishing(cb),"1").start();
        new Thread(new Fishing(cb),"2").start();
        new Thread(new Fishing(cb),"3").start();
        new Thread(new Fishing(cb),"4").start();
    }
    
    static class Fishing implements Runnable{
        CyclicBarrier cb;
        public Fishing(CyclicBarrier cb) {
            this.cb = cb;
        }

        @Override
        public void run() {
            try {
                cb.await();
                System.out.println("第(" + Thread.currentThread().getName() 
                                        + ")個人開始釣魚");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

CountDownLatch和CyclicBarrier的區別:

  • CountDownLatch強調的是一個線程(或多個)需要等待另外的n個線程幹完某件事情之後才能繼續執行。
  • CyclicBarrier強調的是n個線程,大家相互等待,只要有一個沒完成,所有人都得等着。
  • CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。

四、Semaphore(推薦)


Semaphore——信號量,控制同時訪問某個特定資源的線程數量,用在流量控制。

Semaphore可以控同時訪問的線程個數,一個信號量有且僅有3種操作,且它們全部是原子的:初始化、增加和減少。其包含兩個構造方法:

 //參數permits表示許可數目,即同時可以允許多少線程進行訪問
public Semaphore(int permits) {         
    sync = new NonfairSync(permits);
}
 //這個多了一個參數fair表示是否是公平的,即等待時間越久的越先獲取許可
public Semaphore(int permits, boolean fair) {   
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

acquire()用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許可。
release()用來釋放許可。注意,在釋放許可之前,必須先獲獲得許可。

//獲取一個許可
public void acquire() throws InterruptedException {  }     
//獲取permits個許可
public void acquire(int permits) throws InterruptedException { }    
//釋放一個許可
public void release() { }          
//釋放permits個許可
public void release(int permits) { }    

source: Java併發編程:CountDownLatch、CyclicBarrier和Semaphore

五、Callable、Future和FutureTask

java.lang.Runnable接口,在它裏面只聲明瞭一個run()方法,由於run()方法返回值爲void類型,所以在執行完任務之後無法返回任何結果。如果想要返回結果,則可以採用Callable。

  • Callable接口位於java.util.concurrent包下,裏面只聲明瞭一個方法call()
public interface Callable<V> {
    V call() throws Exception;
}
  • 若需要執行Callable接口的方法,還需配合ExecutorService來調用submit()方法使之執行:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
  • Future,對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • FutureTask,對Future接口的具體實現(也是唯一實現),既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值。
public FutureTask(Callable<V> callable) { }
public FutureTask(Runnable runnable, V result) { }

實際使用的時候,可先定義Callble類、並重寫call方法:

public class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子線程在進行計算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

接着可以有兩種方式去啓動有返回值的線程方法:

  • 結合ExecutorService開啓多線程
//創建線程池
ExecutorService executor = Executors.newCachedThreadPool();
//創建Callable對象任務  
Task task = new Task();
//提交任務並獲取執行結果  
Future<Integer> result = executor.submit(task);
//關閉線程池,拿完線程就可以把池子關閉了,及時釋放資源
executor.shutdown();
  • 結合Thread開啓多線程
//構造一個有返回值的並行任務        
Task task = new Task();
//構造FutureTask        
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
//構造一個新線程並注入FutureTask       
Thread thread = new Thread(futureTask);
//開啓線程
thread.start();
  • 執行get()方法會阻塞到線程執行完得到結果
try {
     if(futureTask.get()!=null){  
        System.out.println("task運行結果"+futureTask.get());
     }else{
        System.out.println("future.get()未獲取到結果"); 
     }
} catch (ExecutionException e) {
     e.printStackTrace();
}

source: Java併發編程:Callable、Future和FutureTask

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章