JAVA并行编程-第八章 线程池的使用-学习总结

第八章 线程池的使用

1.在任务与执行策略之间的隐性耦合

有些类型的任务需要明确地指定执行策略,包括:
依赖性任务。大多数行为正确的任务都是独立的:它们不依赖于其他任务的执行时序、执行结果或其他效果。当在线程池中执行独立的任务时,可以随意地改变线程池的大小和配置,这些修改只会对执行性能产生影响。如果提交给线程池的任务需要依赖其他任务,那么就隐含了给执行策略带来了约束,此时必须小心维持这些执行策略以避免产生活跃性问题。
使用线程封闭机制的任务。与线程池相比,单线程的Executor能够对并发性做出更强的承诺。对象可以封闭在任务线程中,使得在该线程中执行的任务在访问对象时不需要同步,即使这些资源不是线程安全也没问题。这种情形将在任务与执行策略形成隐性耦合——任务要求其执行所在的Executor是单线程的(确保任务不会并发执行,并提供足够的同步机制,使得一个任务对内存的作用对于下一个任务一定是可见的)。
对响应时间敏感的任务。GUI应用对响应时间是敏感的:如果用户在点击按钮后需要很长延迟才能得到可见的反馈,那么他们会感到不满。如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程池的线程池中,那么将降低由该Executor管理的服务的响应性。
使用ThreadLocal的任务。ThreadLocal使得每个线程都可以拥有某个变量的一个私有“版本”。只要条件允许,Executor可以自由地重用这些线程。

1.1.线程饥饿死锁

线程饥饿死锁(Thread Starvation Deadlock):所有正在执行任务的线程都由于等待其他仍然处于工作队列中的任务而阻塞。

1.2.运行时间较长的任务

在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待时间超时,那么可以把任务标识为失败,这种办法都能确保任务总能继续执行下去,并将线程释放出来以执行一些能够更快完成的任务。
如果总是线程池充满了被阻塞的任务,那么也可能表明线程池规模太小。

2.设置线程池的大小

线程池的大小通过某种配置机制来提供或者根据Runtime.availableProcessors来动态计算。
cup周期不是唯一影响线程池的大小的资源,还有内存、文件句柄、套接字句柄和数据库连接。计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所得结果就是线程池大小的上限。
当任务需要某种通过资源池来管理的资源时,例如数据库连接,线程池和资源池的大小将会相互影响。

3.配置ThreadPoolExecutor

ThreadPoolExecutor通用构造函数

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime,
						  TimeUnit unit,
						  BlockingQueue<Runnable> workQueue,
						  ThreadFactory threadFactory,
						  RejectedExecutionHandler handler) {...}
					

3.1.线程的创建与销毁

线程池的基本大小(CorePoolSize)最大大小(MaximumPoolSize)以及存活时间等因素共同负责线程的创建与销毁。
基本大小:线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了才会创建超出这个数量的线程。当线程池的当前大小超过了基本大小,这个线程将会被终止。
最大大小:可同时活动的线程数量的上限。
存活时间:某个线程的空闲时间超过了存活时间,那么将被标记为可回收的。

  • newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。
  • newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并将超时设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。
  • 其他形式的线程池可以通过显示的ThreadP构造函数来构造。

3.2.管理队列任务

如果新的请求的到达速率超过了线程池的处理速率,那么新到达的请求将累积起来。子啊线程池中,这些请求会在一个由Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。通过一个Runnable和一个链表节点来表示一个等待中的任务。

基本的任务排列方法有三种:无界队列、有界队列和同步移交(Synchronous Handoff)。

  • newFixedThreadpool和newSingleThreadExecutor在默认的情况下将使用一个无界的LinkedBlockingQueue。如果所有工作线程都在忙碌中,那么任务将在队列中等候,如果任务持续快速到达,而且超过了线程池处理它们的速度,那么队列将无限制增加。
  • 有界队列ArrayBlockingQueue,有界的LinkedBlockingQueue、PriorityBlockingQueue。在使用有界的工作队列时,队列大小和线程池大小必须一起调节。线程池小而队列大,有助于减少内存使用量,降低cpu的使用率,同时减少上下文切换,但是限制吞吐量。
  • 非常大或者无界的线程池,使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作着线程。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等地啊,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交更高效,因为任务会直接移交给执行它的线程,而不是首先放在队列中,然后由工作者线程从队列中提取任务。只有当线程池是无界或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedThreadPool工厂方法中就使用了SynchronousQueue。
  • 使用PriorityBlockingQueue,这个队列通过优先级安排任务。任务优先级通过自然顺序或Comparator(如果任务实现了Comparable)来定义的。

只有当任务是相互独立时,为线程池设置界限才是合理的。如果任务之间存在依赖性,有界的线程池或队列可能导致线程的饥饿死锁,应用无界线程池例如newCacheThreadPool。

3.3.饱和策略

当有界队列被填满后,饱和策略开始发挥作用。
jdk提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy和DiscardOldestPolicy。

  • 中止(Abort)策略是默认的饱和策略,该策略将抛出未检查的RejectedExecution-Exception。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
  • 当新提交的任务无法保存在队列中等待时,抛弃(Discard)策略会悄悄抛弃该任务。
  • Discard-Oldest策略会抛弃下一个即将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么抛弃最旧的策略会抛弃优先级最高的任务,不适合一起使用)。
  • 调用者运行(Caller-Runs)策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在调用了execute的线程中执行该任务。我们可以将WebServer示例修改为使用有界队列和调用者运行饱和策略,当线程池中所有的线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行。由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理正在执行的任务。在此期间,主线程不会调用accept,因此到达的请求将被保护在tcp层的队列中而不是在应用程序队列中。如果持续过载,那么tcp层将最终发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器过载时,那么tcp层将最终发现他的请求队列被填满,因此同样会开始抛弃请求,当服务器过载时,这种过载情况会逐渐向外蔓延——从线程池到工作队列到应用程序再到tcp层,最终到达客户端,导致服务器在高负载下实现一种平缓的性能降低。

3.4.线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。通过指定一个线程工厂方法,可以定制线程池的配置信息。在ThreadFactory中只定义一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。

3.5.在调用构造函数后再定制ThreadPoolExecutor

在调用玩ThreadPoolExecutor的构造函数后,仍然可以通过设置函数Setter来修改大多数传递给它的构造函数(线程池的基本大小、最大大小、存活时间、线程工厂以及拒绝执行处理器(Rejected Execution Handler))如果Executor是通过Executors中的某个工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。

4.扩展ThreadPoolExecutor

ThreadPoolExecutor是扩展的,它提供了几个可以子啊子类中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
在执行任务的线程中将调用beforeExecute和afterExecute等方法,在这些方法中还可以添加日志、计时、监视或统计信息收集的功能。无论任务是从run中正常返回,还是抛出一个异常返回,afterExecute都会被调用。(除了完成后带着一个Error)
如果beforeExecute抛出一个RuntimeException,那么任务将不会被执行,并且afterExecute也不会被调用。
在线程池完成关闭操作时调用teminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者收集finalize统计信息等操作。

5.递归算法的并行化

如果循环中迭代操作都是独立的,并且不需要等待所有迭代操作都完成在继续执行,那么就可以使用Executor将串行循环转化为并行循环。
调用processInParallel比调用processSequentially更快地返回,因为processInParallel会在所有下载任务都进入了Executor的队列后就立即返回,而不会等待这些任务全部完成。如果需要提交一个任务集并等待它们完成,那么可以使用ExecutorService。invokeAll,并且在所有任务都执行完成后调用CompletionService来获取结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值