JAVA并发编程笔记(三)-应用线程池(ThreadPoolExecutor)

任务执行框架Executor,可以简化任务与下线程生命周期的管理,它提供一种简便、灵活的方式,可以在任务的提交和执行策略之间进行解耦。尽管其提供了相当大的灵活性,但并非所有的任务都都能适合所有的执行策略,有些类型的任务需要明确地指定一个执行策略。

一:任务与执行策略间的隐性耦合

1:依赖性任务:提交到线程池的任务需要依赖其他任务,这样就隐性的给执行策略带来了约束。当线程池的任务都是独立的时候,可以随意改变池的长度和配置,这样不会有影响到性能以外的任何事情。

2:采用线程限制的任务:单线程化的Executor比线程池更能保证线程不会并发地执行,假如单线程的Executor改为线程池的话就会失去线程安全性。

3:对响应时间敏感的任务:例如将一个长时间运行的任务提交到单线程的Executor,或者多个长时间运行的任务提交给只包含少量线程的线程池中,这样会削弱由Executor管理的服务的响应性。

4:使用ThreadLocal的任务:ThreadLocal让每个线程可以保留一份变量的私有“版本”。只有当线程本地值的生命周期被限制在当前的任务中时,在池的某线程中使用ThreadLocal才有意义,在线程池中不应该用ThreadLocal传递任务间的值。

当任务都是同类的、独立的时候,线程池才会有最佳的工作表现。如果将耗时的与短期的任务混合在一起,除非池很大,不然会有“塞车”的风险;如果提交的任务需要依赖其他任务,除非池无限,不然会产生死锁的风险。

线程饥饿死锁:在一个大的线程中,如果所有线程执行的任务都阻塞在线程池中,等待着仍然处于同一工作队列中的其他任务,这样就会产生线程饥饿死锁问题。无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且在代码或者配置文件中以及其他可以配置Executor的地方,任何池的大小和配置约束都要写入文档。

二:定制线程池的大小

为了正确定制线程池的长度,你需要理解你的计算环境、资源预算和任务的自身特性。比如:部署系统中安装了多少个CPU?多少内存?任务主要执行的是计算、I/O还是一些混合操作?以下是计算公式:

当然,CPU周期并不是唯一你可以使用线程池管理的资源,其他可以约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等,计算资源池的大小:累加出每个任务需要的资源总量,除以可用的总量,得到的结果是池大小的上限。

当任务需要使用池化的资源时,比如数据库连接,那么线程池和资源池的长度会相互影响。

三:ThreadPoolExecutor

ThreadPoolExecutor为一些Executor提供了基本的实现,通过Executors工厂提供newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool等的返回。ThreadPoolExecutor是一个灵活的、健壮的池实现,允许各种各样的用户定制。如果不能满足要求,可以通过构造函数实例化一个ThreadPoolExecutor,直到满足要求。

    //corePoolSize 核心池的大小 maximumPoolSize 最大池的大小 keepAliveTime 存活时间
    //unit 时间单位 workQueue 队列管理等待执行的任务 threadFactory 线程工厂
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

3.1 管理队列任务

有限的线程池限制了可以并发执行的任务数量(单线程化的Executor是一个值得注意的特例,它们保证没有并发执行的任务,通过线程限制,提供了获得线程安全的可能性)。在可以处理的范围内,线程池并发的执行任务,当请求的速度超过处理的速度,Executor提供队列管理多出的请求,让请求在Runnable队列中等待,而不是作为竞争CPU资源的线程队列。

如果请求快过服务器可以处理它们的速度,仍然存在耗尽资源的可能。如果任务快速的到来你必须遏制住请求的到达率,以避免耗尽内存,这个可以通过饱和策略来优化。

ThreadPoolExecutor允许开发者提供一个BlockingDeque来持有等待执行的任务,任务排队有3种基本方法:无限队列、有限队列和同步移交(synchronous handoff)。

newFixedThreadPool和newSingleThreadExecutor默认使用的是一个无限的LinkedBlockingQueue。如果所有的工作者线程都在工作,任务在队列中等待,如果任务持续快速到达,超过执行速度,队列会无限增加。一个比较稳妥的资源管理策略是使用有限队列,比如:ArrayBlockingQueue或者有限的LinkedBlockingQueue以及PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但是任务队列满时,又会引发一系列问题,这些问题可以结合饱和策略处理。对于一个有界队列,队列的长度与池的长度必须一起调节,一个大队列加一个小池,可以控制对内存和CPU的使用,还可以减少上下文切换,但是要接受潜在吞吐量约束的开销。

对于一个庞大或无线的池,你可以使用SynchronousQueue,完全绕开队列,将任务直接从生产者移交给工作者线程,SynchronousQueue并不是一个真正的队列,而是一种管理直接在线程间移交信息的机制。为了把一个线程放入到SynchronousQueue中,必须有另外一个线程进行接收。不然,当池的大小还小于最大值时,ThreadPoolExecutor就会一直创建新线程,否则根据饱和策略,任务会被拒绝。这样处理会更加高效。只有当池是无限的,或者可以接受任务被拒绝,SynchronousQueue才是一个有价值的选择。例子:Executors.newCachedThreadPool。

使用LinkedBlockingQueue或ArrayBlockingQueue这种FIFO(先进先出)的队列,任务会按照它们到达的顺序开始执行。不能控制执行顺序,可以使用PriorityBlockingQueue,通过优先级安排任务,自然顺序或者Comparator都可以定义优先级。

注意:有限线程或有限队列只有当任务彼此独立时才可以,如果有任务相互依赖,会引起线程饥饿锁,这种情况下可以使用无限队列解决。

3.2:饱和策略

当一个有限队列充满后,饱和策略开始起作用,ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改(如果任务提交到一个已经关闭的Executor时,也会用到饱和策略),JDK提供了AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy几种不同的饱和策略。

AbortPolicy(中止策略)会引起execute抛出未受检查的RejectedExecutionException,调用者可以捕获这个异常,编写自己需要的代码。

DiscardPolicy(遗弃策略):会默认放弃这个任务。DiscardOldestPolicy(遗弃最旧策略):选择丢弃任务,是本应该接下来就执行的任务,该策略还会尝试去重新提交新任务,(如果工作队列是优先级队列,那么遗弃最旧策略丢弃的却是优先级最高的元素,所以这个策略不可与优先级队列是不可混用的)。

CallerRunsPolicy(调用者运行策略):既不会丢弃那个任务,也不会抛出任何异常,它会把一些任务推回到调用者哪里,以此来减缓新任务流,它不会在线程池中执行最新提交的任务,但是它会在一个调用了execute的线程中执行。

使用Semaphore(信号量)也可以实现这个效果,Semaphore会限制任务注入率,设置Semaphore的限制范围等于在池的大小上加上你允许的可以排队的任务数量。

3.3:线程工厂

线程池创建新线程,都需要通过线程工厂来完成,默认的线程工厂(DefaultThreadFactory)创建一个新的,非后台的线程,并没有特殊配置。当然你也可以指明一个线程工厂,能允许你定制线程池线程的配置信息。ThreadFactory只有一个newThread方法,它会在线程池创建新线程时进行调用。

3.4:ThreadPoolExecutor的几个扩展方法

ThreadPoolExecutor提供了几个“钩子”,可以供子类去扩展,分别是:beforeExecute、afterExecute和terminated,这些可以用来扩展ThreadPoolExecutor的行为。

执行任务的线程会调用钩子函数beforeExecute和afterExecute,可以添加日志、时序、监视器和统计信息收集的功能。无论任务正常从run中返回,还是跑出一个异常,afterExecute都会被调用,(如果任务执行后抛出一个Error,则afterExecute不被调用),如果beforeExecute抛出一个RuntimeException,任务将不会被执行,afterExecute也不会被调用。

terminated钩子会在线程池完成关闭动作后调用,也就是所有任务都已经完成并且所有工作者线程也已经关闭后会执行terminated,terminated可以用来释放Executor生命周期里分配到的资源,还可以发出通知、记录日志或者完成统计信息。

总结:对于并发执行的任务,Executor框架是强大且灵活的,它提供了大量可调节的选项,比如创建和关闭线程的策略,处理队列任务的策略,处理过剩任务的队列,并且提供几个钩子方法用于扩展它的行为。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值