《Java并发编程实战》----(八)线程池的使用

1,线程饥饿死锁

在线程池中,如果任务依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,等待第一个任务完成,而第一个任务又无法完成,因为它在等待第二个任务的完成。在更大的线程池中,如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,也会发生统一的问题。这种现象叫做线程饥饿死锁(Thread Starvation Deadlock),只要线程池中的任务需要无限期等待一些必须由池中其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程饥饿死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

2,运行时间较长的任务

如果任务阻塞时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池堵塞,甚至会增加执行时间较短任务的服务时间。如果线程池中的数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有线程都会运行这些执行时间较长的任务,从而影响整体的响应性。

缓解这个问题的技术就是限定等待资源的时间,而不是无限制等待。例如Thraed.join(),BlockingQueue.put()、CountDownLatch.await()等,如果等待超时,可以把任务标识为失败,然后终止任务或将任务重新放回队列以便随后执行。

3,设置线程池的大小

只要避免过大和过小两种极端情况,如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。如果线程池过小,那么导致许多空闲的处理器无法执行工作,从而降低吞吐率。

要想正确设置线程池的大小,必须分析计算环境、资源预算和任务的特性。

在对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N + 1时,通常能实现最优的利用率。

对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,必须估算出任务的等待时间与计算时间的比值。有个公式:

N-----CPU的数量  = Runtime.getRuntime().availableProcessors();

U-----预期CPU利用率

W/C ------等待时间除计算时间

线程池的最优大小 = N * U * (1 + W/C)

4,配置ThreadPoolExecutor

如果newCachedThreadPool、newFixedTheadPool和newScheduledTheadPool等工厂方法返回的ThreadPoolExecutor无法满足需求,可以通过ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己需求来定制。

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

    ...
}

corePoolSize: 线程池基本大小,就是线程池的目标大小,即没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超过这个数量的线程。

maximumPoolSize:线程池的最大大小,表示可同时活动的线程数量的上限。

keepAliveTime:线程的存活时间,如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程会被终止。

5,管理队列任务

newFixedThreadPool和newSingleTheradExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等待。如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。

一种更稳妥的资源管理策略时使用有界队列,例如ArrayBlockingQueue、有界LinkedBlockingQueue、PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但又带来新的问题:当队列满后,新的任务怎么办?在使用有界的工作队列时,队列的大小和线程池的大小必须一起调节,如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU使用率,同时减少上下文切换,但代价是限制了吞吐量。

对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,它可以直接将任务从生产者移交给工作者线程。SynchronousQueue并不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须由另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor会创建一个新的线程来处理这个任务。否则,根据饱和策略,这个任务将被拒绝。直接使用移交将更搞笑,因为任务直接移交给执行它的线程,而不是先放到队列,然后再由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedThreadPool中就是使用了SynchronousQueue。

当使用像LinkedBlockingQueue或ArrayBlockingQueue这样FIFO队列时,任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockingQueue,这个队列根据优先级来安排任务,任务的优先级是通过自然顺序或者Comparator来定义的。

只有当任务相互独立时,为线程池或工作队列设置界限才是合理的 。如果任务之间有依赖性,那么有界的线程池或队列会导致线程饥饿死锁问题,此时应该使用无界的线程池,如newCachedThreadPool。

6,线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。在TheadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。

原文地址:https://www.cnblogs.com/IvySue/p/7487620.html