【读后感】Java Concurrency in Practice:5.任务执行

0. 好想做个 哥布林

为了写出并发性能更加“隽永”的代码,我们需要厘清并组织好任务的逻辑关系:
首先我们将任务抽象成 离散的工作单元
然后将多个任务组织成程序的结构
再根据自然事务(其实就是业务需求)的边界情况来处理错误(超时等等)恢复过程
最后在一些地方使用并行的计算来提升并发性
(总之,本章就是阐述 任务执行框架&任务设计思想)

1. 在线程中执行任务

从任务设计的角度思考(比如说服务器无法预料到请求到达的情况),因为自然会想将任务设计得相互独立,这样可以获得更好的调度、更容易负载均衡,最终实现高吞吐、快速响应的美好愿望。因此,这段废话的意思就是说 我们得从思考并得到清晰的 任务边界(说破了就是线程的最小粒度,但这里我们先配合一波作者) 出发来解决问题。

以web服务器应用为例,这些应用大多都会自然而然的选择一种任务边界——以独立的客户请求为边界。

于是,我们开始了拿手的极端思考方式:

1.1 串行地执行任务(就一条线程梭哈到底)

存在一些问题:
网络波动(程序难以控制的)、SocketIO(将带来阻塞)、本身就不能避免的计算(还不排除这是个大运算的情况)、阻塞后面来的请求。

这样串行的执行很容易带来 低吞吐、灵敏性拉跨的响应、服务器资源利用率感人(CPU在IO阻塞期间将空闲)

在某些情况下,串行处理方式能带来简单性或安全性(GUI就这么搞的)

1.2 显式地为任务创建线程(为每个任务分配一条线程)

主线程不断地交替指向"接受外部连接"(SocketIO)、“分发请求”(dispatch),然后循环创建新线程来处理请求。

可以得到一下推论:
将任务处理从主线程中剥离 => 减少了对后面来的请求的阻塞
并发地处理任务 => 更好的CPU利用率
任务处理的代码会被并发调用 => 必须保证其线程安全性

这种方法可以替代串行执行,并且提升性能的前提是:请求到达速率不超过服务器对请求的处理能力。

1.3 无限制创建线程的不足

前者存在的缺陷:

线程生命周期的开销非常高,因为线程的创建过程需要JVM、操作系统(这将导致不同平台的开销不同)提供一些辅助操作,大多服务器应用的请求处理的开销 较 线程创建而言,都是轻量级的。

资源消耗,活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置,这些闲置的线程会占据许多内存,也给后面的垃圾回收器带来压力,而且大量的线程还会增加CPU资源的竞争(同时会带来其他的开销)

稳定新,在可创建线程的数量上存在一个限制,这个限制值随平台的不同而不同,并且受到多个因素的制约:JVM启动参数、Thread构造函数中请求的栈大小以及底层操作系统对线程的限制。破坏这些限制将导致OOM,并且要像这种错误中恢复过来是非常危险的,更简单的办法是避免超出这些限制。

在一定范围内,增加线程可以提供系统的吞吐率,超出这个范围,再创建线程只会降低程序的执行速度,甚至导致应用程序崩溃。

如果服务器需要提供高可用性,并且在高负载情况下能平缓地降低性能,那么过多的创建线程将成为严重的故障。

在32位的机器上,其中一个主要限制因素是线程栈的地址空间。每个线程都维护两个执行栈,一个用于Java代码,一个用于原生代码。通常,JVM在默认情况下会生成一个复合的栈,其大小可以通过JVM标志-Xss或Thread的构造函数来修改这个值。

2. Executor 框架

j.u.c提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行执行的主要抽象不是Thread,而是Executor。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并且还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制性能监视等机制。

Executor可以用于简化 产消模型,任务的提交的操作就像是生产者,执行任务的操作则是消费者。


package java.util.concurrent;

/**
 * @since 1.5
 * @author Doug Lea
 */
public interface Executor {
    void execute(Runnable command);
}

2.1 执行策略

通过Executor将任务的提交和执行解耦开来之后,我们可以专注于执行的策略:
在什么线程中执行任务?
任务按什么顺序(FIFO、LIFO、优先级)执行?
有多少个任务能并发执行?
在队列中有多少个任务在等待执行?
如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个任务?
另外,如何通知应用程序有任务被拒绝?
在执行一个任务之前或之后,应该进行哪些动作?

最佳策略取决于可用的计算资源&服务质量的需求。

通过限制限制并发任务的数量,可以确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源上发生竞争而严重影响性能(相当于企业应用程序中事务监视器的作用:它能将事物的执行速率控制在某个合理水平,因而就不会使有限资源耗尽或者造成过大压力)。

通过将任务的提交和执行策略分离开来,有助于在部署阶段选择与可用硬件资源最匹配的执行策略。

2.2 线程池

线程池是与工作队列密切相关的。工作线程的任务很简单:从工作队列中获取下一个任务,然后返回线程池等待下一个任务。

“在线程池中执行任务”比“为每个任务分配一个线程”优势更多:
通过重用线程而不是创建新线程,可以…略
通过适当适当的调整线程池的大小,创建足够多的线程以使得处理器保持忙碌状态,同时还可以防止过多的线程相互竞争资源而使得应用程序耗尽内存或失败(可以使服务器的性能缓慢的降低)。
web服务器不会在高负载情况下失败(尽管服务器不会因为创建过多的线程而失败,但在足够长的时间内,如果任务到达的速度总是超过任务执行速度,那么服务器仍然有可能耗尽内存,因为等待执行的Runnable队列在不断增长,可以通过使用有界的工作队列在Executor内部解决这个问题)
还可以实现调优、管理、监视、记录日志、错误报告和其他功能。


类库提供了一个灵活的线程池以及一些有用的默认配置,可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool:最大长度固定,但是某个线程由于未预期的Exception而结束,那么线程池会补充一个新的线程。
newCachedThreadPool:规模不存在任何的限制
newSingleThreadExecutor:是一个单线程的Executor,按照队列的顺序串行执行(FIFO、LIFO、优先级)
newScheduledThreadPool:长度固定 & 支持延迟 & 定时方式执行任务

package com.weng.cloud.service8881.executor;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;
import java.security.AccessControlException;
import sun.security.util.SecurityConstants;

/**
 * @since 1.5
 * @author Doug Lea
 */
public class Executors {
    
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }
    
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool(parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
    }
    
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool(Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
    }
    
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
    }
    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
    }
    
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));
    }
    
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    }
    
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory);
    }
    
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1));
    }
    
    public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
        return new DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1, threadFactory));
    }
    
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);}
    
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

    private Executors() {}
}

2.3 Executor的生命周期

因为JVM只有在所有非守护线程全部终止后才会退出,那么必须正确的关闭Executor。

由于Executor以异步方式执行任务,因此在任何时刻,之前提交的任务状态不是立即可见的。并且应用被关闭的时机也难以预料:有可能采用最平缓的方式(完成了所有启动的任务,并没有接受新的任务),或者最粗暴的方式(直接拔掉机房的电源)。既然Executor是为引用程序提供服务的,因为它们也是可关闭的,并将在关闭操作中受影响的任务的状态反馈给应用程序。


为了解决服务的生命周期问题,Executor扩展了ExecutorService接口。ExecutorService的生命周期有3中状态:运行、关闭和已终止。
shutdown() 将执行平缓的关闭过程,等待所有已调度的任务都执行完成,不再接受新的任务
shutdownNow() 将执行粗暴的关闭过程,尝试取消所有运行中的任务,并且不再启动队列中尚未开始的任务。
awaitTermination() 等待ExecutorService到达终止状态(awaitTermination()后立即调用shutdown()可以达到同步关闭ExecutorService的效果)
isTerminated() 轮询ExecutorService是否已经终止

package java.util.concurrent;

import java.util.List;
import java.util.Collection;

/**
 * @since 1.5
 * @author Doug Lea
 */
public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

ExecutorService关闭后提交的任务将由"拒绝执行处理器"Rejected Exector Handler处理。它会抛弃任务,或者使得execute()抛出一个RejectExecutionException。

2.4 延迟任务与周期任务

Timer类负责管理延迟任务,然而,Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。

Timer支持基于绝对时间,此时对于系统时钟的变化就非常敏感,而ScheduledThreadPoolExecutor只支持基于相对时间的调度。

Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精准性,线程池能弥补这个缺陷。

Timer的另外一个问题是,如果TimerTask抛出一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因为当TimerTask抛出未检查异常时将终止定时线程。在这种情况下,Timer也不会恢复线程的执行,而是错误的认为整个Timer都被取消了。那么已经被调度但尚未执行的TimerTask就不会被执行,新的任务也不能被调度(这个问题也被称为“线程泄露”)。

JDK 5 中,将很少使用Timer。建议使用DelayQueue(已实现BlockingQueue),它可以为ScheduledThreadPoolExectuor提供调度,每个Delayed对象都有一个相应的延迟时间:在DelayedQueue中,只有某个元素逾期后,才能从DelayQueue 中执行take操作。从DelayQueue中返回的对象根据它们的延迟时间进行排序。

3. 找出可利用的并行性

通过将问题分解为多个独立的任务并发执行,能够获得更高的CPU利用率和 响应灵敏度。

3.1 携带结果的任务Callable与Future

Callable 和 Future 有助于表示这些协同任务之间的交互。

Future.get()的异常处理代码将处理两个可能的问题:任务遇到一个Exception,或者调用get的线程在获得结果之前被中断。

3.2 在异构任务并行化中存在的局限

通过对异构任务进行并行化来获得重大的性能提升是很困难的。

如果没有在相似的任务之间找出细粒度的并行性,那么这种方法(给两个任务分配不同的工作)带来的好处将减少。只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。

3.3 CompletionService:Executor与BlockingQueue

如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get(),同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但是有些繁琐。幸运的是,还有一种更好的方法:完成任务CompletionService。

CompletionService将Executor和BlockingQueue的功能融合到一起。你可以将Callable任务提交给它来执行。当计算完成时,可以使用类似队列的操作(take、poll)来获取结果Future.

ExecutorCompletionService实现了CompletionService,并将计算部分委托给了一个Executor。其实现非常简单:在构造中创建一个BQ<Future>保存计算完成的结果,当计算完成时,调用Future-Task中的done()。当提交某个任务时,该任务将首先被包装为一个QueueingFuture(Future的一个子类),然后改写子类的done(),并将结果存入BQ。

多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。因此,CompletionService的作用就相当于一组计算的句柄,这与Future作为单个计算的句柄是非常相似的。

3.4 为任务设置时限

在有限时间内执行任务的主要困难在于,要确保得到答案的时间不会超过限定的时间,或者在限定的时间内无法获得答案。在支持时间限制的Future.get()中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。

在使用限时任务时需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。可以选择在超时后中止执行或取消任务,或者是提前中止它,以免消耗过多的资源。

Future.get()支持传入超时参数(指定时限-当前时间),这个超时参数可能为负,但是无需多虑(j.u.c中所有与实现相关的方法都将负数作为0处理)。

Future.cancel(true)表示任务线程可以在运行过程中中断执行。

ExecutorService.invokeAll支持批量提交任务,并批量的返回异步任务的执行结果。通过这个方法也可以将“预定时间”这一做法轻易的扩展到任意数量的任务上。当超出指定的时限之后,任何还未完成的任务都将取消。当invokeAll返回后,每个任务要么正常地完成工作,要么被取消。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肯尼思布赖恩埃德蒙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值