任务和线程池笔记
构造一个新的线程开销有些大,因为这涉及与操作系统的交互。如果你的程序中创建了大量的生命期很短的线程,那么不应该把每个任务映射到一个单独的线程,而应该使用线程池(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。该池会取消尚未开始的所有任务。
下面总结了在使用连接池时所做的事:
- 调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。
- 调用submit提交Runnable或Callable对象。
- 保存好返回Future对象,以便得到结果或者取消任务。
- 当不想再提交任何任务时,调用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
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池增加很多阻塞任务,会让它无法有效工作。

浙公网安备 33010602011771号