Java核心技術 併發3

8.Callable與Future

Runnable封裝一個異步運行的任務,可以把它想象成一個沒有參數和返回值的異步方法。Callable與Runnable類似,但是有返回值。Callable接口是一個參數化的類型,只有一個方法call。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

類型參數是返回值的類型。
Future保存異步計算的結果,可以啓動一個計算,將Future對象交給某個線程,然後忘掉它。Future對象的所有者在結果計算好之後就可以獲得它:

public interface Future<V> {
	// 取消該計算,如果計算還沒開始,它被取消且不再開始。如果計算處於運行中,那麼如果參數爲true,它就被中斷
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    // 如果計算還在進行,返回false,如果完成了,返回true
    boolean isDone();
    // 調用被阻塞,直到計算完成。如果在計算完成之前,下面一個get方法調用超時,拋出一個TimeoutException。如果運行該計算的線程被中斷,兩個方法將拋出InterruptedExcaption。如果計算已經完成,那麼立即返回。
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask包裝器,可將Callable轉換成Future和Runnable,它同時實現二者的接口。

public class FutureTest {
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in)) {
            System.out.print("Enter base directory (e.g. /use/local/jdk1.8.0/src):");
            String directory = in.nextLine();
            System.out.print("Enter keyword (e.g. volatile):");
            String keyword = in.nextLine();

            MatchCounter counter = new MatchCounter(new File(directory), keyword);
            // 利用MatchCounter創建一個FutureTask對象,並用來啓動一個線程
            FutureTask<Integer> task = new FutureTask<>(counter);
            Thread t = new Thread(task);
            t.start();
            try {
            	// 對get的調用會發生阻塞,直到有可獲得的結果爲止
                System.out.println(task.get() + " matching files");
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {}
        }
    }
}
// 一個需要長時間運行的任務,它產生一個整數值
class MatchCounter implements Callable<Integer> {
    private File directory;
    private String keyword;

    public MatchCounter(File directory, String keyword) {
        this.directory = directory;
        this.keyword = keyword;
    }

    @Override
    public Integer call() throws Exception {
        int count = 0;
        try {
            File[] files = directory.listFiles();
            List<Future<Integer>> results = new ArrayList<>();
			// 使用遞歸機制,對於每一個子目錄,產生一個新的MatchCounter併爲它啓動一個線程
            for (File file : files) {
                if (file.isDirectory()) {
                    MatchCounter counter = new MatchCounter(file, keyword);
                    FutureTask<Integer> task = new FutureTask<>(counter);
                    results.add(task);
                    Thread t = new Thread(task);
                    t.start();
                } else {
                    if (search(file)) {
                        count++;
                    }
                }
            }
			// 把FutureTask對象隱藏在ArrayList<Future<Integer>>中,最後把所有結果加起來
            for (Future<Integer> result : results) {
                try {
                    count += result.get();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        } catch (InterruptedException e){}
        return count;
    }

    public boolean search(File file) {
        try {
            try (Scanner in = new Scanner(file, "UTF-8")) {
                boolean found = false;
                while (!found && in.hasNextLine()) {
                    String line = in.nextLine();
                    if (line.contains(keyword)) {
                        found = true;
                    }
                }
                return found;
            }
        } catch (IOException e) {
            return false;
        }
    }
}

每一次對get的調用都會發生阻塞直到結果可獲得爲止。當然,線程是並行運行的,因此,很大可能在大致相同的時間所有結果都可獲得。

9.執行器

如果程序中創建了大量的生命週期很短的線程,應該使用線程池(thread pool)。一個線程池中包含許多準備運行的空閒線程。將Runnable對象交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,而是在池中準備爲下一個請求提供服務。
另一個使用線程池的理由是減少併發線程的數目。創建大量線程會大大降低性能甚至使用虛擬機崩潰。如果有一個會創建許多線程的算法,應該使用一個線程數“固定的”線程池以限制併發線程的總數。
執行器(Executor)類有許多靜態工作方法用來創建線程池:
在這裏插入圖片描述

線程池

newCachedThreadPool方法構建了一個線程池,對於每個任務,如果有空閒線程可用,立即讓它執行任務,如果沒有可用的空閒線程,則創建一個新線程。
newFixedThreadPool方法構建一個具有固定大小的線程池。如果提交的任務數多於空閒的線程數,那麼把得不到服務的任務放置到隊列中。當任務完成以後再運行它們。
newSingleThreadExecutor是一個退化了的大小爲1的線程池:由一個線程執行提交的任務,一個接着一個。
這3個方法返回實現了ExecutorService接口的ThreadPoolExecutor類的對象。
可用下面的方法將一個Runnable對象或Callable對象提交給ExecutorService:
Future< ? > submit(Runnable task)
Future< T > submit(Runnable task, T result)
Future< T > submit(Callable< T > task)
該池會在方便的時候儘早執行提交的任務。調用submit時,會得到一個Future對象,可用來查詢該任務的狀態。
第一個submit方法返回一個奇怪樣子的Future< ? >。可以使用這樣一個對象來調用isDone、cancel或isCancelled。但get方法在完成的時候只是簡單地返回null。
第二個submit也提交一個Runnable,並且Future的get方法在完成的時候返回指定的result對象。
第三個submit提交一個Callable,並且返回的Future對象將計算結果準備好的時候得到它。

當用完一個線程池的時候,調用shutdown。該方法啓動該池的關閉序列。被關閉的執行器不再接受新的任務。當所有任務都完成以後,線程池中的線程死亡。
另一種方法是調用shutdownNow,該池取消尚未開始的所有任務並試圖中斷正在運行的線程。

在使用線程池時應該做的事:
1.調用Executor類中的靜態方法newCachedThreadPool或newFixedThreadPool
2.調用submit提交Runnable或Callable對象
3.如果想要取消一個任務,或如果提交Callable對象,那麼就要保存好返回的Future對象
4.當不再提交任何任務時,調用shutdown

public class ThreadPoolTest {
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in)) {
            System.out.print("Enter base directory (e.g. /use/local/jdk1.8.0/src):");
            String directory = in.nextLine();
            System.out.print("Enter keyword (e.g. volatile):");
            String keyword = in.nextLine();

            ExecutorService pool = Executors.newCachedThreadPool();

            MatchCounter counter = new MatchCounter(new File(directory), keyword, pool);
            Future<Integer> result = pool.submit(counter);

            try {
                System.out.println(result.get() + " matching files");
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {}

            pool.shutdown();
			// 出於信息方面的考慮,打印池中最大的線程數。但不能通過ExecutorService這個接口得到。因此必須將該pool對象強制轉爲ThreadPoolExecutor對象
            int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
            System.out.println("largest pool size=" + largestPoolSize);
        }
    }
}

class MatchCounter implements Callable<Integer> {
    private File directory;
    private String keyword;
    private ExecutorService pool;
    private int count;

    public MatchCounter(File directory, String keyword, ExecutorService pool) {
        this.directory = directory;
        this.keyword = keyword;
        this.pool = pool;
    }

    @Override
    public Integer call() {
        count = 0;
        try {
            File[] files = directory.listFiles();
            List<Future<Integer>> results = new ArrayList<>();

            for (File file : files) {
                if (file.isDirectory()) {
                    MatchCounter counter = new MatchCounter(file, keyword, pool);
                    Future<Integer> result = pool.submit(counter);
                    results.add(result);
                } else {
                    if (search(file)) {
                        count++;
                    }
                }
            }
            for (Future<Integer> result : results) {
                try {
                    count += result.get();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        } catch (InterruptedException e) {}
        return count;
    }

    public boolean search(File file) {
        try {
            try (Scanner in = new Scanner(file, "UTF-8")) {
                boolean found = false;
                while (!found && in.hasNextLine()) {
                    String line = in.nextLine();
                    if (line.contains(keyword)) {
                        found = true;
                    }
                }
                return found;
            }
        } catch (IOException e) {
            return false;
        }
    }
}

預定執行

ScheduledExecutorService接口具有爲預定執行(Scheduled Executor)或重複執行任務而設計的方法。它是一種允許使用線程池機制的java.util.Timer的泛化。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法將返回實現了ScheduledExecutorService接口的對象。
可以預定Runnable或Callable在初始的延遲之後只運行一次。也可以預定一個Runnable對象週期性地運行。

控制任務組

使用執行器有更有實際意義的原因,控制一組相關任務。
invokeAny方法提交所有對象到一個Callable對象的集合中,並返回某個已經完成了的任務的結果。無法知道返回的究竟是哪個任務的結果。對於搜索問題,如果願意接受任何一種解決方案的話,就可以使用這個方法。例如,對一個大整數進行因數分解計算來解碼RSA密碼。可以提交多個任務,每個任務使用不同範圍內的數來分解。只要一個任務得到答案,計算就可以停止了。
invokeAll方法提交所有對象到一個Callable對象的集合中,並返回一個Future對象的列表,代表所有的解決方案。當計算結果可獲得時:

List<Callable<T>> tasks = ...;
List<Future<T>> results = executor.invokeAll(tasks);
for(Future<T> result : results) {
	processFurther(result.get());
}

這個方法的缺點是如果第一個任務花去了很多時間,則可能不得不進行等待。將結果按可獲得的順序保存起來更有實際意義。可以用ExecutorCompletionService來進行排序。
用常規方法獲得一個執行器。然後構建一個ExecutorCompletionService,提交任務給完成服務(completion service)。該服務管理Future對象的阻塞隊列,其中包含已經提交的任務的執行結果(當這些結果成爲可用時)。這樣一來,相比前面的計算,一個更有效的組織形式:

ExecutorCompletionService<T> service = new ExecutorCompletionService<>(executor);
for (Callable<T> task : tasks) {
	service.submit(task);
}
for (int i = 0; i < tasks.size(); i++) {
	processFurther(service.take().get());
}

Fork-Join框架

有些應用使用了大量線程,但其中大多數都是空閒的。例如,一個Web服務器可能會爲每一個連接分別使用一個線程。另一些可能對每個處理器內核分別使用一個線程,來完成計算密集型任務,如圖像或視頻處理。Java SE 7中新引入了fork-join框架,專門用來支持後一類應用。假設有一個處理任務,它可以很自然地分解爲子任務。
一個例子,統計一個數組中有多少個元素滿足某個特定的屬性。可以將這個數組一分爲二,分別對兩部分進行統計,再將結果相加。

public class ForkJoinTest {
    public static void main(String[] args) {
        final int SIZE = 10000000;
        double[] numbers = new double[SIZE];
        for (int i = 0; i < SIZE; i++) {
            numbers[i] = Math.random();
        }
        Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5);
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(counter);
        System.out.println(counter.join());
    }
}
// 要採用框架可用的一種方式完成遞歸計算,需要提供一個擴展RecursiveTask<T>的類(結果爲T)或提供一個擴展RecursiveAction的類(不生 成結果)
class Counter extends RecursiveTask<Integer> {
    public static  final int THRESHOLD = 1000;
    private double[] values;
    private int from;
    private int to;
    private DoublePredicate filter;

    public Counter(double[] values, int from, int to, DoublePredicate filter) {
        this.values = values;
        this.from = from;
        this.to = to;
        this.filter = filter;
    }
	// 覆蓋compute方法來生成並調用子任務,然後合併其結果
    @Override
    protected Integer compute() {
        if (to - from < THRESHOLD) {
            int count = 0;
            for (int i = from; i < to; i++) {
                if (filter.test(values[i])) {
                    count++;
                }
            }
            return count;
        } else {
            int mid = (from + to) / 2;
            Counter first = new Counter(values, from, mid, filter);
            Counter second = new Counter(values, mid, to, filter);
            // invokeAll方法接收很多任務並阻塞,知道所有任務都完成,join方法將生成結果。
            invokeAll(first, second);
            return first.join() + second.join();
        }
    }
}

在後臺,fork-join框架使用了一種有效的智能方法來平衡可用線程的工作負載,這種方法稱爲工作密取(work stealing)。每個工作線程都有一個雙端隊列(deque)來完成任務。一個工作線程將子任務壓入其雙端隊列的對頭。只有一個線程可以訪問對頭,所以不需要加鎖。一個工作線程空閒時,它會從另一個雙端隊列的隊尾密取一個任務。由於大的子任務都在隊尾,這種密取很少出現。

可完成Future

處理非阻塞調用的傳統方法是使用事件處理器,程序員爲任務完成之後要出現的動作註冊一個處理器。當然,如果下一個動作也是異步的,在它之後的下一個動作會在一個不同的事件處理器中。儘管程序員會認爲“先做步驟1,然後是步驟2,再完成步驟3”,但實際上程序邏輯會分散到不同的處理器中。如果必須增加錯誤處理,情況會更糟糕。假設步驟2是“用戶登錄”。可能需要重複這個步驟,因爲用戶輸人憑據時可能會出錯。要嘗試在一組事件處理器中實現這樣一個控制流,或者想要理解所實現的這樣一組事件處理器,會很有難度。
Java SE 8的CompletableFuture類提供了一種候選方法。與事件處理器不同,“可完成future”可以“組合”(composed)。

例如,假設我們希望從一個Web頁面抽取所有鏈接來建立一個網絡爬蟲:

// Web頁面可用時這會生成這個頁面的文本
public void CompletableFuture<String> readPage(URL url)
// 生成一個HTML頁面中的URL,可以調度當頁面可用時再調用這個方法
public static List<URL> getLinks(String page)

CompletableFuture<String> contents = readPage(ur1);
// thenApply方法不會阻塞。它會返回另一個future。第一個future 完成時,其結果會提供給getLinks方法,這個方法的返回值就是最終的結果
CompletableFuture<List<URL>> links = contents.thepply(Parser:getLinks);

利用可完成future,可以指定你希望做什麼,以及希望以什麼順序執行這些工作。當然,這不會立即發生,不過重要的是所有代碼都放在一- 處。
從概念上講,CompletableFuture 是一個簡單API,不過有很多不同方法來組合可完成future。下面先來看處理單個future的方法(如表所示)。(對於這裏所示的每個方法,還有兩個Async 形式,不過這裏沒有給出,其中一種形式使用一個共享ForkJoinPool, 另一種形式有一個Executor參數)。在這個表中,我使用了簡寫記法來表示複雜的函數式接口,這裏會把Function< ? superT, U>寫爲T->U。當然這並不是真正的Java類型。
在這裏插入圖片描述
你已經見過thenApply方法。以下調用:

CompletableFuture<U> future.thenApply(f);
CompletableFuture<U> future.thenApplyAsync(f);

會返回一個future,可用時會對future 的結果應用f。第二個調用會在另一個線 程中運行f。

thenCompose方法沒有取函數T-> U,而是取函數T ->CompletableFuture< U>。這聽上去相當抽象,不過實際上也很自然。考慮從一個給定URL讀取一個Web頁面的動作。不用提供方法:
public String blockingReadPage(URL url)
更精巧的做法是讓方法返回一個future:
public CompletableFuture< String> readPage(URL url)
現在,假設還有一個方法可以從用戶輸人得到URL,這可能從一個對話框得到,而在用戶點擊OK按鈕之前不會得到答案。這也是將來的一一個事件:
public CompletableFuture< URL> geuRLTnut(string prompt)
這裏我們有兩個函數T-> CompletableFuture< U>和U -> CompletableFuture< V>。顯然,如果第二個函數在第一個函數完成時調用,它們就可以組合爲一個函數T ->CompletableFuture< V>。這正是thenCompose所做的。

上表中第3個方法強調了目前爲止我一直忽略的另一個方面:失敗( failure)。CompletableFuture中拋出一個異常時,會捕獲這個異常並在調用get方法時包裝在一個受查異常ExecutionException中。不過,可能get永遠也不會被調用。要處理異常,可以使用handle方法。調用指定的函數時要提供結果(如果沒有則爲nul)和異常(如果沒有則爲null),這種情況下就有意義了。其餘的方法結果都爲void,通常用在處理管線的最後。下面來看組合多個future 的方法:
在這裏插入圖片描述
前3個方法並行運行一個CompletableFuture< T>和一個CompletableFuture< U >動作,並組合結果。
接下來3個方法並行運行兩個CompletableFuture< T>動作。一旦其中一個動作完成,就傳遞它的結果,並忽略另一個結果。
最後的靜態allof和anyOf方法取一組可完成future(數目可變),並生成一個CompletableFuture< Void>,它會在所有這些future都完成時或其中任意一個future 完成時結束。不會傳遞任何結果。

10.同步器

java.util.concurrent 包包含了幾個能幫助管理相互合作的線程集的類(見表)。這些機制具有爲線程之間的共用集結點模式( common rendezvous patterns)提供的“預置功能”( canned functionality)。 如果有一個相互合作的線程集滿足這些行爲模式之一,那麼應該直接重用合適的庫類而不要試圖提供手工的鎖與條件的集合。
在這裏插入圖片描述

信號量

概念上講,一個信號量管理許多的許可證( permit)。爲了通過信號量,線程通過調用acquire請求許可。其實沒有實際的許可對象,信號量僅維護一個計數。許可的數目是固定的,由此限制了通過的線程數量。其他線程可以通過調用release釋放許可。而且,許可不是必須由獲取它的線程釋放。事實上,任何線程都可以釋放任意數目的許可,這可能會增加許可數目以至於超出初始數目。
信號量在1968年由Edsger Djkstra發明,作爲同步原語( synchronization primitive )。Dijkstra指出信號量可以被有效地實現,並且有足夠的能力解決許多常見的線程同步問題。在幾乎任何一本操作系統教科書中,都能看到使用信號量實現的有界隊列。
當然,應用程序員不必自己實現有界隊列。通常,信號量不必直接映射到通用應用場景。

倒計時門栓

一個倒計時門栓( CountDownLatch)讓一個線程集等待直到計數變爲0。倒計時門栓是一次性的。一旦計數爲0,就不能再重用了。
一個有用的特例是計數值爲1的門栓。實現一個只能通過一次的門。線程在門外等候直到另一個線程將計數器值置爲0。
舉例來講,假定一個線程集需要一些初始的數據來完成工作。工作器線程被啓動並在門外等候。另一個線程準備數據。當數據準備好的時候,調用countDown,所有工作器線程就可以繼續運行了。
然後,可以使用第二個門栓檢查什麼時候所有工作器線程完成工作。用線程數初始化門栓。每個工作器線程在結束前將門栓計數減1。另一個獲取工作結果的線程在門外等待,一旦所有工作器線程終止該線程繼續運行。

障柵

CyclicBarrier類實現了一個集結點( rendezvous)稱爲障柵( brrier)。考慮大量線程運行在一次計算的不同部分的情形。當所有部分都準備好時,需要把結果組合在一起。 當一個線程完成了它的那部分任務後,我們讓它運行到障柵處。一旦所有的線程都到達了這個障柵,障柵就撤銷,線程就可以繼續運行。
下面是其細節:

// 構造一個障柵,並給出參與的線程數
CyclicBarrier barrier = new CyclicBarrier(nthreads);
// 每一個線程做一些工作,完成後在障柵上調用await
public void run(){
	doWork();
	barrier.await();
	...
}

await方法有一個可選的超時參數:barrier.await(100, TimeUnit.MILISECONDS);
如果任何一個在障柵上等待的線程離開了障柵,那麼障柵就被破壞了(線程可能離開是因爲它調用await時設置了超時,或者因爲它被中斷了)。在這種情況下,所有其他線程的await方法拋出BrokenBarrierException異常。那些已經在等待的線程立即終止await的調用。
可以提供-一個可選的障柵動作(barrieraction),當所有線程到達障柵的時候就會執行這一動作。

Runnable barrierAction= ...;
CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);

該動作可以收集那些單個線程的運行結果。
障柵被稱爲是循環的(cyclic),因爲可以在所有等待線程被釋放後被重用。在這一點上,有別於CountDownLatch, CountDownLatch只能被使用一次。
Phaser類增加了更大的靈活性,允許改變不同階段中參與線程的個數。

交換器

當兩個線程在同一個數據緩衝區的兩個實例上工作的時候,就可以使用交換器( Exchanger)。典型的情況是,一個線程向緩衝區填入數據,另一個線程消耗這些數據。當它們都完成以後,相互交換緩衝區。

同步隊列

同步隊列是一種將生產者與消費者線程配對的機制。當一個線程調用SynchronousQueue的put方法時,它會阻塞直到另一個線程調用take方法爲止,反之亦然。與Exchanger的情況不同,數據僅僅沿一個方向傳遞, 從生產者到消費者。
即使SynchronousQueue類實現了BlockingQueue 接口,概念上講,它依然不是一個隊列。它沒有包含任何元素,它的size方法總是返回0。

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