任务和线程池笔记

构造一个新的线程开销有些大,因为这涉及与操作系统的交互。如果你的程序中创建了大量的生命期很短的线程,那么不应该把每个任务映射到一个单独的线程,而应该使用线程池(thread pool)。线程池中包含许多准备运行的线程。为线程池提供一个Runnable,就会有一个线程调用run方法。当run方法推出时,这个线程不会死亡,而是留在池中准备为下一个请求提供服务。

1、Callable和Future

Runnable封装一个异步运行的任务,可以把它想象成一个没有参数和返回值的异步方法。Callback和Runnable类似,但是有返回值。Callback接口是一个参数化的类型,只有一个方法call。

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

类型参数是返回值的类型。
Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。这个Future对象的所有者在结果计算好之后就可以获得结果。
Future接口有下面的方法:

public interface Future<V>
{
    V get()
    V get(long timeout, TimeUnit unit)
    void cancel(boolean maylnterrupt)
    boolean isCancelled()
    boolean isDone()
}

第一个get方法的调用被阻塞,直到计算完成。如果在计算完成之前,第二个方法的调用超时,拋出一个TimeoutException异常。如果运行该计算的线程被中断,两个方法都将拋出IntermptedException。如果计算已经完成,那么get方法立即返回。
如果计算还在进行,isDone方法返回false;如果完成了,则返回true。
可以用cancel方法取消该计算。如果计算还没有开始,它被取消且不再开始。如果计算处于运行之中,那么如果maylnterrupt参数为true,它就被中断。
执行Callable的一种方法是使用FutureTask,它实现了Future和Runnable接口,所以可以构造一个线程来运行这个任务:

Callable<Integer> task = ...;
FutureTask futureTask = new FutureTask<Integer>(task);
Thread t = new Thread(futureTask);// it's a Runnable
t.start();
...
Integer result = task.get();// it's a Future

更常见的情况是,可以将一个Callable传递到一个执行器。

2、执行器

执行器(Excutors)类有许多静态工厂方法,用来构造线程池。
执行器工厂方法

方法 描述
newCachedThreadPool 必要时创建新线程;空闲线程会保留60秒
newFixedThreadPool 池中包含固定数目的线程;空闲线程会一直保留
newWorkStealingPool 一种适合"fork-join"任务的线程池,其中复杂的任务会分解为更简单的任务,空闲线程会"密取"较简单的任务
newSingleThreadExecutor 只有一个线程的"池",会顺序地执行所提交的任务
newScheduledThreadPool 用于调度执行的固定线程池
newSingleThreadScheduledExecutor 用于调度执行的单线程"池"

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. 调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。
  2. 调用submit提交Runnable或Callable对象。
  3. 保存好返回Future对象,以便得到结果或者取消任务。
  4. 当不想再提交任何任务时,调用shutdown。
    ScheduledExecutorService接口为调度执行或重复执行任务提供了一些方法。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法返回实现ScheduledExecutorService接口的对象。
    可以调度Runnable或Callable在一个初始延迟之后运行一次。也可以调度Runnable定期运行。
3、控制任务组

有时,使用执行器有更策略性的原因:需要控制一组相关的任务。
invokeAny方法提交一个Callable对象集合中的所有对象,并返回某个已完成任务的结果。我们不知道返回的究竟是哪个任务的结果,这往往是最快完成的那个任务。
invokeAll方法提交一个Callable对象集合中的所有对象,这个方法会阻塞,直到所有任务都完成,并返回表示所有任务答案的一个Future对象列表。得到计算结果后,还可以像下面这样对结果进行处理:

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

在for循环中,第一个result.get()调用会阻塞,直到第一个结果可用。如果所有任务几乎同时完成,这不会有问题。不过,很有必要按计算出结果的顺序得到这些结果。可以利用ExecutorCompletionService来管理。
首先以通常的方式得到一个执行器。然后构造一个ExecutorComplectionService。将任务提交到这个完成服务。该服务会管理Future对象的阻塞队列,其中包含所提交任务的结果(一旦结果可用,就会放入队列)。因此,要完成之前的计算,以下组织更为高效:

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

接下来的程序展示了如何使用callable和执行器。在第一个计算中,我们统计了一个目录树中包含一个给定单词的文件的个数。在程序的第二部分,要搜索包含指定单词的第一个文件。我们使用invokeAny方法来并行化这个搜索。

package com.company.Synchronize12.executors;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 该程序演示了可调用的接口和执行程序
 * Created by kzm on 2020/11/4 21:46
 */
public class ExecutorDemo {
    /**
     * 计算文件中给定单词的出现次数
     */
    public static long occurrences(String word, Path path){
        try (Scanner in = new Scanner(path)){
            int count = 0;
            while (in.hasNext())
                if (in.next().equals(word)) count++;
            return count;
        } catch (IOException e) {
            return 0;
        }
    }

    /**
     * 返回给定目录的所有后代
     */
    public static Set<Path> descendants(Path rootDir) throws IOException {
        try (Stream<Path> entries = Files.walk(rootDir)){
            return entries.filter(Files::isRegularFile).collect(Collectors.toSet());
        }
    }

    /**
     * 产生一个任务,该任务搜索文件中的单词
     */
    public static Callable<Path> searchForTask(String word, Path path){
        return () -> {
            try (Scanner in = new Scanner(path)){
                while (in.hasNext()){
                    if (in.next().equals(word)) return path;
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("Search in " + path + " canceled.");
                        return null;
                    }
                }
                throw new NoSuchElementException();
            }
        };
    }

    public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
        try (Scanner in = new Scanner(System.in)){
            System.out.print("Enter base directory: ");
            String start = in.nextLine();
            System.out.print("Enter keyword: ");
            String word = in.nextLine();

            Set<Path> files = descendants(Paths.get(start));
            List<Callable<Long>> tasks = new ArrayList<>();
            for (Path file : files){
                Callable<Long> task = () -> occurrences(word, file);
                tasks.add(task);
            }
            ExecutorService executor = Executors.newCachedThreadPool();

            Instant startTime = Instant.now();
            List<Future<Long>> results = executor.invokeAll(tasks);
            long total = 0;
            for (Future<Long> result : results)
                total += result.get();
            Instant endTime = Instant.now();
            System.out.println("Occurrences of " + word + ": " + total);
            System.out.println("Time elapsed: " + Duration.between(startTime, endTime).toMillis() + " ms");

            List<Callable<Path>> searchTasks = new ArrayList<>();
            for (Path file : files)
                searchTasks.add(searchForTask(word, file));
            try {
                Path found = executor.invokeAny(searchTasks);
                System.out.println(word + " occurs in: " + found);
            } catch (Exception e){
                e.printStackTrace();
            }

            if (executor instanceof ThreadPoolExecutor)
                System.out.println("Largest pool size: " + ((ThreadPoolExecutor) executor).getLargestPoolSize());
            executor.shutdown();

        }
    }
}
4、fork-join框架

Java7新引入了fork-join框架。假设有一个处理任务,它可以很自然地分解为子任务。如下所示:

if (problemSize < threshold)// threshold: 阈值
      // 直接解决问题
else {
      // 将问题分解为子问题
      // 递归解决每个子问题
      // 结合结果
}

图像处理就是这样的一个例子。要增强一个图像,可以变换上半部分和下半部分。如果有足够多空闲的处理器,这些操作可以并行运行。
在这里,我们将讨论一个更简单的例子。假设想统计一个数组中有多少个元素满足某个特定的属性。可以将这个数组一分为二,分别对这两部分进行统计,再将结果相加。
要采用框架可用的一种方式完成这种递归运算,需要提供一个RecursiveTask的类(如果计算会生成一个类型为T的结果)或者提供一个扩展RecursiveAction的类(如果不生成任何结果)。再覆盖compute方法来生成并调用子任务,然后合并其结果。

package com.company.Synchronize12.forkJoin;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.function.DoublePredicate;

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());
    }
}

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;
    }

    @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(first, second);
            return first.join() + second.join();
        }
    }
}

在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取(work stealing)。每个工作线程都有一个双端队列(deque)来完成任务。一个工作线程将子任务压入其双端队列的队头。(只有一个线程可以访问队头,所以不需要加锁。)一个工作线程空闲时,它会从另一个双端队列的队尾"密取"一个任务。由于大的子任务都在队尾,这种密取很少出现。
fork-join池是针对非阻塞工作负载优化的。如果向一个fork-join池增加很多阻塞任务,会让它无法有效工作。

posted @ 2020-11-14 11:08  luotuoccc  阅读(106)  评论(0)    收藏  举报