《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,每当线程池需要创建一个新线程时都会调用这个方法。

时间: 2024-10-07 19:14:09

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

【java并发编程实战】-----线程基本概念

学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习Java并发编程,共同进步,互相指导. 在学习Java并发之前我们需要先理解一些基本的概念:共享.可变.线程安全性.线程同步.原子性.可见性.有序性. 共享和可变 要编写线程安全的代码,其核心在于对共享的和可变的状态进行访问. "共享"就意味着变量可以被多个线程同时访问.我们知道系统中的资

JAVA - 并发编程 - 执行器和线程池

思考? 1 为什么要使用执行器和线程池? 2 执行器和线程是什么?怎么使用 执行器 线程执行器分离了任务的创建和执行,提高了线程的性能 线程池 避免了频繁地创建和销毁线程,达到线程对象的重用,可以根据项目灵活地控制并发的数量 ExecutorService (java.util.concurrent) 1 Executors.newCachedThreadPool() 可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 2 Executors.newFixedT

java并发编程实战笔记-线程安全性

什么是线程安全性 线程安全性定义中最核心的概念就是:**正确性**.我们将单线程的正确性近似 定义为"所见即所知",当多个线程访问这个类的时候,始终能表现出正确的行为, 那么这个类就是线程安全类. 当多个线程访问某个类时,不管运行时环境采用什么调度方式或者这些线程将如何 交替运行,并且调用代码时,不需要额外的同步,就可以产生正确的结果.这个类 就是线程安全类. 在线程安全类上执行任何串行或者并行的操作都不会使对象处于无效状态. 可重入代码:就是这段代码,和其他代码不存在共享状态,只包含

《Java并发编程实战》第八章 线程池的使用 读书笔记

一.在任务与执行策略之间的隐性解耦 有些类型的任务需要明确地指定执行策略,包括: . 依赖性任务.依赖关系对执行策略造成约束,需要注意活跃性问题.要求线程池足够大,确保任务都能放入. . 使用线程封闭机制的任务.需要串行执行. . 对响应时间敏感的任务. . 使用ThreadLocal的任务. 1. 线程饥饿死锁 线程池中如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,这种现象称为线程饥饿死锁. 2. 运行时间较长的任务 Java提供了限时版本与无限时版本.例如Thread

读书笔记-----Java并发编程实战(一)线程安全性

线程安全类:在线程安全类中封装了必要的同步机制,客户端无须进一步采取同步措施 示例:一个无状态的Servlet 1 @ThreadSafe 2 public class StatelessFactorizer implements Servlet{ 3 public void service(ServletRequest req,ServletResponse resp){ 4 BigInteger i = extractFromRequest(req); 5 BigInteger[] fact

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock

java并发编程实战手册(一)线程管理

本文主要是以知识点的形式对java多线程进行了解,学习java多线程的基础,本文参考书籍<java并发编程实战手册>,若有兴趣想研究跟高级的多线程思想,可以阅读<java并发编程实战>. 1.线程的创建和运行 java线程的创建有三种方式,可能大部分人只知道常用的两种: 1.继承Thread类,并且覆盖run()方法. 2.创建一个实现Runnable接口的类.使用带参数的Thread构造器来创建Thread对象. 3.使用Callable与Future来创建启动线程 1.创建Ca

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显

《Java并发编程实战》读书笔记

Subsections 线程安全(Thread safety) 锁(lock) 共享对象 对象组合 基础构建模块 任务执行 取消和关闭 线程池的使用 性能与可伸缩性 并发程序的测试 显示锁 原子变量和非阻塞同步机制 一.线程安全(Thread safety) 无论何时,只要多于一个线程访问给定的状态变量.而且其中某个线程会写入该变量,此时必须使用同步来协助线程对该变量的访问. 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的. 线程安全的实例: (1).一个无状

《Java并发编程实战》第十一章 性能与可伸缩性 读书笔记

造成开销的操作包括: 1. 线程之间的协调(例如:锁.触发信号以及内存同步等) 2. 增加的上下文切换 3. 线程的创建和销毁 4. 线程的调度 一.对性能的思考 1 性能与可伸缩性 运行速度涉及以下两个指标: 某个指定的任务单元需要"多快"才能处理完成.计算资源一定的情况下,能完成"多少"工作. 可伸缩性: 当增加计算资源时(例如:CPU.内存.存储容器或I/O带宽),程序的吞吐量或者处理能力能相应地增加. 2 评估各种性能权衡因素 避免不成熟的优化.首先使程序正