》》在任务和执行策略之间的隐性耦合
@@@ Executor 框架可以将任务的提交与任务的执行策略解耦开来。
@@@ 虽然 Executor 框架的制定和修改执行策略都提供了相当大的灵活性,但并非所有的任务
都能适用所有的执行策略。有些类型的任务需要明确地指定执行策略,包括:
-------- 依赖性任务
大多数行为正确的任务都是独立的:它们不依赖其他任务的执行时序 、 执行结果或其他
结果。当在线程池中执行独立的任务时,可以随意地改变线程池的大小和配置,这些修改只会
对执行性能产生影响。
然而,如果提交给线程池的任务需要依赖其他的任务,那么就隐含地给执行策略带来了约束,
此时必须小心地维持这些执行策略以避免产生活跃性问题。
-------- 使用线程封闭机制的任务
对象可以封闭在任务线程中,使得在该线程中执行的任务在访问该对象时不需要同步,即使
这些资源不是线程安全的也没有问题。这种情形将在任务与执行策略之间形成隐式的耦合-----任务
要求其执行所在的 Executor 是单线程的。如果将 Executor 从单线程环境改为线程池环境,那么将
会失去线程安全性。
-------- 对响应时间敏感的任务
-------- 使用 ThreadLocal 的任务
ThreadLocal 使每个线程都可以拥有某个变量的私有的 “ 版本” 。 然而,只要条件允许,
Executor 可以自由地重用这些线程。
在标准的 Executor 实现中 ,当执行需要较低时将回收空闲线程,而当需要增加时将添加新的
线程,并且如果从任务中抛出了一个未检查异常,那么将用一个新的工作者线程来替代抛出异常的线程。
只有当线程本地值的生命周期受限于任务的生命周期时,在线程池中使用 ThreadLocal 才有意义,
而在线程池的线程中不应该使用 ThreadLocal 在任务之间传递值。
@@@ 只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。
@@@ 在基于网络的典型服务器应用程序中------网页服务器 、 邮件服务器 以及文件服务器等,它们的
请求通常都是同类型的并且相互独立的。
@@@ 在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会
要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务
需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略
而破坏安全性与活跃性。
### 线程饥饿死锁
@@@ 每当提交一个有依赖性的 Executor 任务时, 要清楚地知道可能会出现线程 “ 饥饿”死锁,
因此,需要在代码或配置 Executor 的配置文件中记录线程的大小限制或配置限制。
### 运行时间较长的任务
@@@ 执行时间较长的任务不仅会造成线程池堵塞,甚至会增加执行时间较短任务的服务时间。
@@@ 如果线程池中线程的数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能
所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。
@@@ 限定任务等待资源的时间可以缓解执行时间较长任务造成的影响。
-------- 在平台类库中大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如
Thread.join 、 BlockingQueue.put 、 CountDownLatch.await 以及 Selector.select 等 。
@@@ 如果在线程池中总是充满了被阻塞的任务,那么也可能表明线程池的规模过小。
》》设置线程池的大小
@@@ 线程池的大小取决于被提交任务的类型以及所部属系统的特性。在代码中通常不会固定
线程池的大小,而应该通过某种机制来提供,或者根据 Runtime.availableProcessors 来动态
计算。
@@@ 要想正确地设置线程池的大小,必须分析计算环境 、 资源预算和任务的特性。在部署的
系统中有多少 CPU ? 多大的内存 ? 任务是计算密集型 、 I / O 密集型还是二者皆可?它们是否
需要像 JDBC 连接这样的稀缺资源?
如果需要执行不同类别的任务,并且它们之间的相差很大,那么应该考虑使用多个线程池,
从而使每个线程池可以根据各自的工作负载来调整。
@@@ 对于计算密集型的任务,在拥有 N(cpu) 个处理器的系统上,当线程池的大小为 N + 1 时,
通常能实现最优的利用率。
@@@ 对于包含 I / O 操作或其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模
应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。这种估算
不需要很精确,并且可以通过一些分析或监控工具来获得。
@@@ 还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池
来运行应用程序,并观察 CPU 利用率的水平。
@@@ 当任务需要某种资源池来管理资源时,例如数据库连接,那么线程池和资源池的大小将会相互
影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的大小。同样,当线程
池的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连接池的大小。
》》配置 ThreadPoolExecutor
@@@ ThreadPoolExecutor 为一些 Executor 提供了基本的实现,这些 Executor 是由 Executors
中的 newCachedThreadPool 、 newFixedThreadPool 和 newScheduledTreadPool 等工厂方法返回的。
ThreadPoolExecutor 是一个灵活的 、 稳定的线程池,允许进行各种定制。
@@@ 如果默认的执行策略不能满足要求,那么可以通过 ThreadPoolExecutor 的构造函数来实例
化一个对象,并根据自己的需求来定制,并且可以参考 Executors 的源代码来了解默认配置下的执行
策略,然后再以这些执行策略为基础进行修改。
### 线程的创建与销毁
@@@ 线程池的基本大小(Core Pool Size )、最大大小(Maximum Pool Size) 以及存活时间
等因素共同负责线程的创建与销毁。
-------- 基本大小:就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只在工作队列满了
的情况下才会创建超出这个数量的线程。
-------- 最大大小:表示可同时活动的线程数量的上限。
-------- 如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程的当前大小
超过了基本大小时,这个线程将被终止。
@@@ 通过调节线程池的大小和存活时间,可以帮助线程池回收空闲线程占有的资源,从而使得这些
资源可以用于执行其他工作。
--------- newFixedThreadPool 工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且
创建的线程池不会超时。
--------- newCachedThreadPool 工厂方法将线程池的最大大小设置为 Integer.MAX_VALUE ,而将基
本大小设置为零,并将超时设置为 1 分钟,这种方法创建出来的线程池可以被无限扩展,并且
当需求降低时会自动收缩。
### 管理队列任务
@@@ 在有限的线程池中会限制可并发执行的任务数量。
@@@ 在线程池中,请求会在一个由 Executor 管理的 Runnable 队列中等待,而不会像线程那样去
竞争 CPU 资源。通过一个 Runnable 和一个链表节点来表现一个等待中的任务,当然比使用线程来表
示的开销低很多,但如果客户提交给服务器请求的速率超过了服务器的处理速率,那么仍可能会耗尽资源。
@@@ ThreadPoolExecutor 允许提供一个 BlockingQueue 来保存等待执行的任务。基本的任务排列
方法有 3 种: 无界队列 、 有界队列 和同步移交(Synchronous Handoff)。
队列的选择与其他的配置参数有关:例如线程池的大小等。
@@@ newFixedThreadPool 和 newSingleThreadExecutor 在默认情况下将使用一个无界的
LinkedBlockingQueue 。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等待。如果任务持续
快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。
@@@ 一种更稳妥的资源管理策略是使用有界队列,例如 ArrayBlockingQueue 、 有界的
LinkedBlockingQueue 、 PriorityBlockingQueue 。
--------- 有界队列有助于避免资源耗尽的情况发生,但它又带来了新的问题:当队列填满后,新的任务该
怎么办?(有许多饱和策略可以解决这个问题)
--------- 在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。
如果线程池较小而队列较大,那么有助于减少内存使用量,降低 CPU 的使用率,同时还可以
减少上下文切换,但付出的代价是可能会限制吞吐量。
@@@ 对于一个非常大的或者无界的线程池,可以通过使用 SynchronousQueue 来避免任务排列,以及
直接将任务从生产者移交给工作者线程。SynchronousQueue 不是一个真正的队列,而是一种在线程之间进行
移交的机制。要将一个元素放入 SynchronousQueue 中, 必须有另一个线程正在等待接受这个元素。如果
没有线程正在等待,并且线程池的当前大小小于最大值,那么 ThreadPoolExecutor 将创建一个新的线程,否
则根据饱和策略,这个任务将被拒绝。
@@@ 只有当线程池是无界的或者可以拒绝任务时, SynchronousQueue 才有实际价值。在
newCachedThreadPool 工厂方法中就使用了 SynchronousQueue 。
@@@ 对于 Executor , newCachedThreadPool 工厂方法是一种很好的默认选择,它能提供比固定大小
的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的
线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。
@@@ 只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,
那么有界的线程池或队列就可能导致线程 “ 饥饿” 死锁问题。此时应该使用无界的线程池,例如
newCachedThreadPool 。
### 饱和策略
@@@ 当有界队列被填满后,饱和策略开始发挥作用。
---------- ThreadPoolExecutor 的饱和策略可以通过调用 setRejectedExecutionHandler 来修改。
(如果某个任务被提交到了一个已经被关闭的 Executor 时, 也会用到饱和策略)
@@@ JDK 提供了几种不同的 RejectedExecutionHandler 实现,每种实现都包含有不同的饱和
策略:AbortPolicy 、 CallerRunsPolicy 、 DiscardPolicy 和 DiscardOldestPolicy 。
----------- AbortPolicy (中止策略):中止策略是默认的饱和策略,该策略将抛出未检查的
RejectedExecutionException 。调用者可以捕获这个异常,然后根据需求编写自己的
处理代码。
当新提交的任务无法保存到队列中等待执行时,“ DiscardPolicy (抛弃策略)”
会悄悄抛弃该任务。“ DiscardOldestPolicy (抛弃最旧的策略)” 则会抛弃下一个将被
执行的任务,然后尝试重新提交新的任务。
如果工作队列是一个优先级队列,那么“ 抛弃最旧的策略 ” 将导致抛弃优先级
最高的任务,因此最好不要将 “ 抛弃最旧的 ” 饱和策略和优先级队列放在一起使用。
----------- CallerRunsPolicy(调用者运行策略):实现了一种调节机制,该策略既不会抛弃任务,
也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。它不会在线
程池中的某个线程中执行新提交的任务,而是在一个调用了 execute 的线程中执行该
任务。
@@@ 当创建 Executor 时, 可以选择饱和策略或者对执行策略进行修改。
@@@ 当工作队列被填满后,没有预定义的饱和策略来阻塞 execute 。然而,通过使用
Semaphore (信号量) 来限制任务的到达率,就可以实现这个功能。
### 线程工厂
@@@ 每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。
---------- 默认的线程工厂方法将创建一个新的 、 非守护的线程,并且不包含特殊的配置信息。
---------- 通过指定一个线程工厂的方法,可以定制线程池的配置信息。
---------- 在 ThreadFactory 中只定义了一个方法 newThread( ) ,每当线程池需要创建一个新
线程时都会调用这个方法。
----------- 在许多情况下都需要使用定制的线程工厂方法。
@@@ 如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通
过 Executor 中的 privilegedThreadFactory 工厂来定制自己的线程工厂。通过这种方式创建出来
的线程,将与创建 privilegedThreadFactory 的线程拥有相同的访问权限 、 AccessControlContext
和 contextClassLoader 。
如果不使用 privilegedThreadFactory ,线程池创建的线程将从在需要新线程时调用
execute 或 submit 的客户程序中继承访问权限,从而导致令人困惑的安全性异常。
### 在调用构造函数后再定制 ThreadPoolExecutor
@@@ 如果 Executor 是通过 Executors 中的某个(除了 newSingleThreadExecutor)工厂方法
创建的,那么可以将结果的类型转换为 ThreadPoolExecutor 以访问设置器(set....( ) 方法)
@@@ 在 Executors 中包含一个 unconfigurableExecutorService 方法 ,该方法对一个现有的
ExecutorService 进行包装,使其只暴露 ExecutorService 的方法,因此不对它进行修改配置。
@@@ 如果将 ExecutorService 暴露给不信任的代码,又不希望对其进行修改,就可以通过
unconfigurableExecutorService 来包装它。
》》扩展ThreadPoolExecutor
@@@ ThreadPoolExecutor 是可扩展的,它提供了几个可以在子类中改写的方法:beforeExecute 、
afterExecute 和 terminated , 这些方法可以用于扩展 ThreadPoolExecutor 的行为。
---------- 无论任务是从 run 中正常返回,还是抛出一个异常而返回,afterExecutor 方法都会被调用。
(如果任务在完成后带有一个 Error , 那么就不会调用 afterExecutor)
--------- 如果 beforeExecute 抛出一个 RuntimeException ,那么任务将不被执行,并且
afterExecutor 也不会被调用。
---------- 在线程池完成关闭操作时调用 terminated ,也就是在所有任务都已经完成并且所有工作
者线程也已经关闭后。terminated 可以用来释放 Executor 在其生命周期里分配的各种资源,此外,
还可以执行发送通知 、 记录日志或者收集 finalize 统计信息等操作。
》》递归算法的并行化
》》小结
@@@ 对于并发执行的任务, Executor 框架是一种强大且灵活的框架。它提供了大量可调节的选项,
例如创建线程和关闭线程的策略,处理队列任务的策略,处理过多任务的策略,并且提供了几个钩子
方法来扩展它的行为。
然而,与大多数功能强大的框架一样,其中一些设置参数并不能很好地工作,某些类型的任务
需要特定的执行策略,而一些参数组合则可能产生奇怪的结果。