Java多线程——线程池

Executor类(产生线程池)

构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。

Executor类有许多静态工厂方法用来构建线程池,下表对这些方法进行了汇总。

方法描述
newCachedThreadPool必要时创建新线程,空闲线程保留60秒
newFixedThreadPool该池包含固定数量的线程;空闲线程会一直被保留
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务(类似于Swing事件分配线程)
newScheduledThreadPool用于预定执行而创建的固定线程池,替代java.util.Timer
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对象,可用来查询该任务的状态。

当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。
另一种方法是调动shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。

下面总结了在使用线程池时应该做的事:
1) 调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。
2) 调用submit提交Runnable或Callable对象。
3) 如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象。
4) 当不再提交任何任务时,调用shutdown。

预定执行(ScheduledExecutorService)

ScheduledExecutorService接口具有为预定执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。

可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性的运行。

控制任务组(ExecutorCompletionService)

你已经了解了如何将一个执行器服务作为线程池使用,以提高执行任务的效率。有时,使用执行器有更有实际意义的原因:控制一组相关任务。例如,可以在执行器中使用shutdownNow方法取消所有的任务。

invokeAny方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以像下面这样对结果进行处理:

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

这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排列。
用常规的方法获得一个执行器。然后,构建一个ExecutorCompletionService,提交任务给完成服务(completion service)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。这样一来,相比前面的计算,一个更有效的组织形式如下:

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

Fork-Join框架(ForkJoinPool)

有些应用使用了大量线程,但其中大多数都是空闲的。举例来说,一个Web服务器可能会为每个连接分别使用一个线程。另外一些应用可能对每个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。Java SE 7中新引入了fork-join框架,专门用来支持后一类应用。假设有一个处理任务,它可以很自然的分解为子任务,如下所示:

f(problemSize < threshold)
    //直接处理问题
else{
    //分解问题成小问题
    //递归处理各个小问题
    //合并结果
}

图像处理就是这样一个例子。要增强一个图像,可以变换上半部分和下半部分。如果有足够多空闲的处理器,这些操作可以并行运行。(除了分解为两部分外,还需要做一些额外的工作,不过这属于技术细节,我们不做讨论)。

要采用框架可用的一种方式完成这种递归计算,需要提供一个扩展RecursiveTask的类(如果计算会生成一个类型为E的结果)或者提供一个扩展RecursiveAction的类(如果不生成任何结果)。

可完成Future(CompletedFuture)

处理非阻塞调用的传统方法是使用事件处理器,程序员为任务完成之后要出现的动作注册一个处理器。当然,下一步动作也是异步的,在它之后的下一个动作会在一个不同的事件处理器中。尽管程序员会认为“先做步骤1,然后是步骤2,再完成步骤3”,但实际上程序逻辑会分散到不同的处理器中。如果必须增加错误处理,情况会更糟糕。假设步骤2是“用户登录”。可能需要重复这个步骤,因为用户输入凭据时可能会出错。要尝试在一组事件处理器中实现这样一个控制流,或者想要理解所实现的这样一组事件处理器,会很有难度。

Java SE 8的CompletableFuture类提供了一种候选方法。与事件处理器不同,“可完成future”可以“组合”(composed)。

例如,假设我们希望从一个Web页面抽取所有链接来建立一个网络爬虫。下面假设有这样一个方法:

ublic void CompletableFuture<String> readPage(URL url)

Web页面可用时会生成这个页面的文本。如果方法:

ublic static List<URL> getLinks(String page)

生成一个HTML页面中的URL,可以调度当页面可用时再调用这个方法:

CompletableFuture<String> contents = readPage(url);
CompletableFuture<List<URL>> links = contents.thenApply(Parser::getLinks);

thenApply方法不会阻塞。它会返回另一个future。当第一个future完成时,其结果会提供给getLinks方法,这个方法的返回值就是最终的结果。

利用可完成future,可以指定你希望做什么,以及希望以什么顺序执行这些工作。当然这不会立即发生,不过重要的是所有代码都放在一处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值