Java理论和实践:线程池和工作队列

发表于:2015-2-28 09:42

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Defonds    来源:51Testing软件测试网采编

  Java 多线程编程论坛中最常见的一个问题就是各种版本的 "我怎么样才可以创建一个线程池?" 几乎在每个服务器应用里,都会出现关于线程池和工作队列的问题。本文中,Brian Goetz 就线程池原理、基本实现和调优技术、需要避开的一些常见误区等方面进行共享。
  为何要用线程池?
  有很多服务器应用,比如 Web 服务器,数据库服务器,文件服务器,或者邮件服务器,都会面对处理大量来自一些远程请求的小任务。请求可能会以很多种方式到达服务器,比如通过一种网络协议(比如 HTTP,FTP 或者 POP),通过一个 JMS 队列,或者通过轮询数据库。不管请求是如何到达的,对于服务应用来说通常是每个独立任务的处理很短暂但是请求的数量却很大。
  构建一个服务器应用的很简单的一个模式就是在每个请求到达时新创建一个线程并在该线程中对该次请求进行处理。这种方式对于样品级应用来说工作的很好,但当你想要将其部署作为一个服务器应用时,很多明显的缺陷开始凸显出来。这种 "线程-每-请求" 的方式的缺点之一是:为每个请求都新创建一个线程的开销是巨大的;为每个请求新建一个线程将会花费更多时间,而且服务器在线程的创建和销毁的资源开销上会比其实际处理用户请求的过程的开销还要大。
  除了线程的创建和销毁的开销之外,活动的线程也会占用一些系统资源。在一台 JVM 中创建过多的线程会导致系统内存溢出,或者会因为过度的内存消耗而带来超负载。为了防止资源超载,服务器应用需要一些手段来对指定时间内有多少请求会被同时处理进行限制。
  线程池就线程生命周期开销和资源不足这两个问题同时给出了解决方案。通过线程的多任务复用,线程的创建开销分摊到了多个任务之上。更多优惠,由于请求到来时该线程已经存在了,所以线程创建所带来的延迟也被消除了。因此,请求可以得到立即服务,这让应用程序响应更快。另外,通过适当地调整线程池中线程的数量,你可以通过强制超过请求阀值时的更多请求去等待直到有一个线程空闲出来去处理它这种手段来防止资源超载。
  线程池的替代方案
  线程池并非服务应用使用多线程的唯一方案。如上文所提到的,某些场景下为每个新任务新建一个线程也是完全合理的。但是,如果任务创建的频率很高而且任务的持续时间很短,为每个任务新建一个线程将会带来性能问题。
  另一个常见的线程模型是为某种特别类型的任务设置一个单独的后台线程和任务队列。AWT 和 Swing 使用了这种模型,其中有一个 GUI 事件线程,任何导致用户接口改变的工作都必须在该线程中执行。但是,因为只有一个 AWT 线程,在 AWT 线程中执行可能需要很长时间来完成的任务是不可取的。因此,Swing 程序常常会为 UI 关联的长时间执行的任务要求额外的工作者线程。
  在特定场景下,"线程-每-任务" 和 "单个-后台-线程" 这两种方式都可以运行的很完美。"线程-每-任务" 方式在具有少量长时间运行任务的场景下工作的很棒。"单个-后台-线程" 方式在调度可预见性不是很重要的场景下工作的很棒,因为这里大都是一些后台运行的低优先级任务。但是,大多数的服务器应用面向的是处理大量短暂的任务或子任务,这时候就希望有一个低开销有效地处理这些任务的机制,而且还要具备资源管理和时间可预见性的一些措施。无疑线程池能够提供给我们这些优点。
  工作队列
  根据线程池的实际实现来看,"线程池" 一词有些误导,一个线程池 "明显的" 实现,在大多数情况下并不会完全产生我们所期望的结果。实际上,"线程池" 一词的出现比 Java 平台的诞生还要早,它很可能是一个缺少面向对象的环境下的产物。但这一名词还是会被广泛地使用下去。
  在客户端类等待一个可用的线程时,我们可以很简单地实现一个线程池类,把任务丢给该线程执行,然后在它执行结束之后将该线程返回给线程池,这种线程池的实现方法有几个潜在的不良影响。比方说,当线程池为空的时候,会发生什么?任何想要传递一个任务给线程池的调用者会发现线程池是空的,调用者线程会阻塞在等待线程池给它一个可用的线程中。通常,我们想要使用后台线程的原因之一就是阻止提交线程的阻塞。这种导致调用者线程阻塞,比如一个线程池 "明显的" 实现的情况,让我们碰到了想来寻求解决的类似一个问题(译者注:调用者线程想要通过新线程防止阻塞,却在请求线程池一个线程的时候发生了阻塞)。
  我们通常想要的是结合了一组工作者线程的一个工作队列,它会使用 wait() 和 notify() 通知等待线程有无新工作到来。这个工作队列一般实现为具有一个相关监控对象的某种链表。以下代码演示了一个简单的池化工作队列。这个模式,使用了一个 Runnable 对象队列,是一个常见的调度和工作队列的规范,尽管 Thread API 没有强加这种特殊要求。
public class WorkQueue {
private final int nThreads;
private final PoolWorker[] threads;
private final LinkedList queue;
public WorkQueue(int nThreads) {
this.nThreads = nThreads;
queue = new LinkedList();
threads = new PoolWorker[nThreads];
for (int i = 0; i < nThreads; i++) {
threads[i] = new PoolWorker();
threads[i].start();
}
}
public void execute(Runnable r) {
synchronized (queue) {
queue.addLast(r);
queue.notify();
}
}
private class PoolWorker extends Thread {
public void run() {
Runnable r;
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException ignored) {
}
}
r = (Runnable) queue.removeFirst();
}
// If we don't catch RuntimeException,
// the pool could leak threads
try {
r.run();
} catch (RuntimeException e) {
// You might want to log something here
}
}
}
}
}
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号