Java多線程——執行器(Executor)

Markdown編輯器上線啦,終於等到你了!使用這個編輯器寫了兩篇,感覺還是不錯的!不過還是有一些問題,慢慢熟悉吧!

執行器

構建一個新的線程是有一定的代價的,因爲涉及到和操作系統的交互。如果程序中創建了大量的生命週期很短的線程,應該使用線程池(thread pool)。

另一個使用線程池的理由是減少併發線程的數目。線程數量太多會大大降低性能甚至會使虛擬機崩潰。如果有一個會創建許多線程的算法,應該使用一個線程數“固定的”線程池以限制併發線程的總數。

Executor類構建線程池的靜態方法

方法 描述
newCachedThreadPool 必要時創建新線程,空閒線程會被保留60秒
newFixedThreadPool 該池包含固定數量的線程;空閒線程會一直被保留
newSingleThreadExecutor 只有一個線程的“池”,該線程順序執行每一個提交的任務
newScheduledThreadPool 用於預定執行而構建的固定線程池,替代java.util.Timer
newSingleThreadScheduledExecutor 用於預定執行而構建的單線程“池”

線程池

newCachedThreadPool方法構建了一個線程池,對於每個任務,如果有空閒的線程可用,立即讓它執行任務,如果沒有可用的空閒線程,則創建一個新線程。

newFixedThreadPool方法構建一個具有固定大小的線程池。如果提交的任務數多於空閒的線程數,那麼把得不到的服務的任務放置到隊列中。當其他任務完成以後再運行它們。

newSingleThreadExecutor是一個退化了的大小爲1的線程池:由一個線程執行提交的任務,一個接着一個。這三個方法返回實現了ExecutorService接口的ThreadPoolExecutor類的對象。

使用連接池:

  1. 調用Executors類中靜態的方法newCachedThreadPool或newFixedThreadPool。
  2. 調用submit提交Runnable或Callable對象。
  3. 如果想要取消一個任務,或如果提交Callable對象,那就要保存好返回的Future對象。
  4. 當不在提交任何任務時,調用shutdown。

MacthCounter類:

/**
 * @author xzzhao
 */
public class MacthCounter implements Callable<Integer> {

    private final File            directory;
    private final String          keyword;
    private final ExecutorService pool;
    private int                   count;

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

    @Override
    public Integer call() throws Exception {
        count = 0;
        File[] files = directory.listFiles();
        List<Future<Integer>> results = new ArrayList<>();
        for (File file : files) {
            if (file.isDirectory()) {
                MacthCounter counter = new MacthCounter(file, keyword, pool);
                FutureTask<Integer> task = new FutureTask<>(counter);
                results.add(task);
            } else {
                if (search(file)) {
                    count++;
                }
            }
        }
        for (Future<Integer> result : results) {
            count += result.get();
        }
        return count;
    }

    /**
     * 搜索方法
     * 
     * @param file
     * @return 是否找到
     */
    public boolean search(File file) {
        try {
            try (Scanner in = new Scanner(file)) {
                boolean found = false;
                while (!found && in.hasNextLine()) {
                    String line = in.nextLine();
                    if (line.contains(keyword)) {
                        found = true;
                    }
                }
                return found;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

ThreadPoolTest類:

/**
 * @author xzzhao
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入根目錄 :");
        String directory = in.nextLine();
        System.out.println("請輸入關鍵字 :");
        String keyword = in.nextLine();

        ExecutorService pool = Executors.newCachedThreadPool();

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

        try {
            System.out.println("匹配到的文檔數:" + result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        pool.shutdown();
        int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
        System.out.println("largestPoolSize=" + largestPoolSize);
    }
}

預定執行

ScheduledExecutorService接口具有爲預定執行或重複執行任務而設計的方法。它是一種允許使用線程機制的Java.util.Timer的泛化。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法將返回實現了ScheduledExecutorService接口的對象。

可以預定Runnable或Callable在初始的延遲之後只運行一次。也可以預定一個Runnable對象週期性地運行。

控制任務組

我們已經知道如何將一個執行器服務作爲線程池使用,以提高執行任務的效率。有時候,我們要使用執行器來做更有實際意義的事,控制一組相關的任務。例如,可以在執行器中使用shutdownNow方法取消所有的任務。

invokeAll方法提交所有對象到一個Callable對象的集合中,並返回一個Future對象的列表,代表所有任務的結果。這個方法的缺點是如果第一個任務花去了很多時間,那麼就可能不得不進行等待。將結果按可獲得的順序保存起來更有意義。可以用ExecutorComeletionService來進行排序。

具體可以查詢API。用常規的方法獲得一個執行器。然後,構建一個ExecutorComeletionService,提交任務給完成服務。該服務管理Future對象的阻塞隊列,其中包含已經提交的任務的執行結果。
大概如下:

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

Fork-Join框架

有的應用程序使用了大量的線程,但其中大多數都是空閒的。舉例來說,一個Web服務器可能會爲每個連接分別使用一個線程。另外一些應用可能對每個處理器內核分別使用一個線程,來完成計算密集的任務,如圖像或視頻處理。Java SE 7 中新引入了fork-join框架,專門用來支持後一類的應用。

我們來討論一個簡單的例子。假設我們想統計一個數組中有多少個元素滿足摸個特定的屬性。可以將這個數組一分爲二,分別對着兩部分進行統計,再將結果相加。

Counter 類:

/**
 * @author xzzhao
 */
public class Counter extends RecursiveTask<Integer> {

    public static final int THRESHOLD = 1000;
    private final double[]  values;
    private final int       from;
    private final int       to;
    private final Filter    filter;

    public Counter(double[] values, int from, int to, Filter filter) {
        super();
        this.values = values;
        this.from = from;
        this.to = to;
        this.filter = filter;
    }

    @Override
    protected Integer compute() {
        if (to - from < THRESHOLD) {
            int count = 0;
            for (int i = from; i < to; i++) {
                if (filter.accept(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(first, second);
            return first.join() + second.join();
        }
    }
}

Counter 類:

/**
 * @author xzzhao
 */
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, new Filter() {
            @Override
            public boolean accept(double x) {
                return x > 0.5;
            }
        });
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(counter);
        System.out.println(counter.join());
    }
}

採用框架可用的一種方式完成這種遞歸計算,需要提供一個擴展RecursiveTask的類,如果計算會生成一個結果的話,或者如果不生成任何結果,就可以提供一個擴展RecursiveAction的類。再覆蓋compute方法來生成並調用子任務,然後合併結果。

invokeAll方法接收很多任務並阻塞,知道所有的這些任務都已經完成。join方法將生成結果。對每個子任務都應用了join,並返回總和。

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