线程创建 |
线程最普遍的一个应用程序是创建一个或多个线程,以执行特定类型的任务。Timer
类创建线程来执行 TimerTask
对象,Swing 创建线程来处理 UI 事件。在这两种情况中,在单独线程中执行的任务都假定是短期的,这些线程是为了处理大量短期任务而存在的。
在其中每种情况中,这些线程一般都有非常简单的结构:
while (true) {
if (no tasks)
wait for a task;
execute the task;
}
通过例示从 Thread
获得的对象并调用 Thread.start()
方法来创建线程。可以用两种方法创建线程:通过扩展 Thread
和覆盖 run()
方法,或者通过实现 Runnable
接口和使用 Thread(Runnable)
构造函数:
class WorkerThread extends Thread {
public void run() { /* do work */ }
}
Thread t = new WorkerThread();
t.start();
或者:
Thread t = new Thread(new Runnable() {
public void run() { /* do work */ }
}
t.start();
重新使用线程 |
因为多个原因,类似 Swing GUI 的框架为事件任务创建单一线程,而不是为每项任务创建新的线程。首先是因为创建线程会有间接成本,所以创建线程来执行简单任务将是一种资源浪费。通过重新 使用事件线程来处理多个事件,启动和拆卸成本(随平台而变)会分摊在多个事件上。
Swing 为事件使用单一后台线程的另一个原因是确保事件不会互相干涉,因为直到前一事件结束,下一事件才开始处理。该方法简化了事件处理程序的编写。使用多个线 程,将要做更多的工作来确保一次仅一个线程地执行线程相关的代码。
如何不对任务进行管 理 |
大多数服务器应用程序(如 Web 服务器、POP 服务器、数据库服务器或文件服务器)代表远程客户机处理请求,这些客户机通常使用 socket 连接到服务器。对于每个请求,通常要进行少量处理(获得该文件的代码块,并将其发送回 socket),但是可能会有大量(且不受限制)的客户机请求服务。
用于构建服务器应用程序的简单化模型会为每个请求创建新的线程。下列代码段实现简单的 Web 服务器,它接受端口 80 的 socket 连接,并创建新的线程来处理请求。不幸的是,该代码不是实现 Web 服务器的好方法,因为在重负载条件下它将失败,停止整台服务器。
class UnreliableWebServer {
public static void main(String[] args) {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable r = new Runnable() {
public void run() {
handleRequest(connection);
}
};
// Don't do this!
new Thread(r).start();
}
}
}
当服务器被请求吞没时,UnreliableWebServer
类不能很好地处理这种情况。每次有请求时,就会创建新的类。根据操作系统和可用内存,可以创建的线程数是有限的。不幸的是,您通常不
知道限制是多少 ——
只有当应用程序因为 OutOfMemoryError
而崩
溃时才发现。
如果足够快地在这台服务器上抛出请求的话,最终其中一个线程创建将失败,生成的 Error
会关闭整个应用程序。当一次仅能有效支持很少线程时,没有必要创建上千个线程,无论如何,这样使用资源可能会损害性能。创建线程会使用相当一部分内存,其
中包括有两个堆栈(Java 和 C),以及每线程数据结构。如果创建过多线程,其中每个线程都将占用一些 CPU
时间,结果将使用许多内存来支持大量线程,每个线程都运行得很慢。这样就无法很好地使用计算资源。
使用线程池解决问题 |
为任务创建新的线程并不一定不好,但是如果创建任务的频率高,而平均任务持续时间低,我们可以看到每项任务创建一个新的线程将产生性能(如果负载不可预 知,还有稳定性)问题。
如果不是每项任务创建一个新的线程,则服务器应用程序必须采取一些方法来限制一次可以处理的请求数。这意味着每次需要启动新的任务时,它不能仅调用下列代 码。
new Thread(runnable).start()
管理一大组小任务的标准机制是组合工作队列和线程池。工作队列就是要处理的任务的队列,前面描述的 Queue
类完全适合。线程池是线程的集合,每个线程都提取公用工作队列。当一个工作线程完成任务处理后,它会返回队列,查看是否有其他任务需要处理。如果有,它会
转移到下一个任务,并开始处理。
线程池为线程生命周期间接成本问题和资源崩溃问题提供了解决方案。通过对多个任务重新使用线程,创建线程的间接成本将分布到多个任务中。作为一种额外好 处,因为请求到达时,线程已经存在,从而可以消除由创建线程引起的延迟。因此,可以立即处理请求,使应用程序更易响应。而且,通过正确调整线程池中的线程 数,可以强制超出特定限制的任何请求等待,直到有线程可以处理它,它们等待时所消耗的资源要少于使用额外线程所消耗的资源,这样可以防止资源崩溃。
Executor框架 |
java.util.concurrent
包中包含灵活的线程池实现,但是更重要的是,它包含用于管理实现 Runnable
的任务的执行的整个框架。该框架称为 Executor 框架。
Executor
接口相当简单。它描述将运行 Runnable
的对象:
public interface Executor {
void execute(Runnable command);
}
任务运行于哪个线程不是由该接口指定的,这取决于使用的 Executor
的实现。它可以运行于后台线程,如 Swing
事件线程,或者运行于线程池,或者调用线程,或者新的线程,它甚至可以运行于其他 JVM!通过同步的 Executor
接口提交任务,从任务执行策略中删除任务提交。Executor
接口独自关注任务提交 —— 这是 Executor
实现的选择,确定执行策略。这使在部署时调整执行策略(队列限制、池大小、优先级排列等等)更加容易,更改的代码最少。
java.util.concurrent
中的大多数 Executor
实现还实现 ExecutorService
接口,这是对 Executor
的扩展,它还管理执行服务的生命周期。这使它们更易于管理,并向生命可能比单独 Executor
的生命更长的应用程序提供服务。
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout,
TimeUnit unit);
// other convenience methods for submitting tasks
}
Executor |
java.util.concurrent
包包含多个 Executor
实现,每个实现都实现不同的执行策略。什么是执行策略?执行策略定义何时在哪个线程中运行任务,执行任务可能消耗的资源级别(线程、内存等等),以及如果
执行程序超载该怎么办。
执行程序通常通过工厂方法例示,而不是通过构造函数。Executors
类包含用于构造许多不同类型的 Executor
实现的静态工厂方法:
-
Executors.newCachedThreadPool()
创建不限制大小的线程池,但是当以前创建的线程可以使用时将重新使用那些线程。如果没有现有线程可用,将创建新的线程并将其添加到池中。使用不到 60 秒的线程将终止并从缓存中删除。 -
Executors.newFixedThreadPool(int n)
创建线程池,其重新使用在不受限制的队列之外运行的固定线程组。在关闭前,所有线程都会因为执行过程中的失败而终止,如果需要执行后续任务,将会有新的线 程来代替这些线程。 -
Executors.newSingleThreadExecutor()
创建 Executor,其使用在不受限制的队列之外运行的单一工作线程,与 Swing 事件线程非常相似。保证顺序执行任务,在任何给定时间,不会有多个任务处于活动状态。
更可靠的 Web 服务器 —— 使用 Executor |
前面 《如何不对任务进行管理》 中的代码显示了如何不用编写可靠服务器应用程序。幸运的是,修复这个示例非常简单,只需将 Thread.start()
调用替换为向 Executor
提交任务即可:
class ReliableWebServer {
Executor pool =
Executors.newFixedThreadPool(7);
public static void main(String[] args) {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable r = new Runnable() {
public void run() {
handleRequest(connection);
}
};
pool.execute(r);
}
}
}
注意,本例与前例之间的区别仅在于 Executor
的创建以及如何提交执行的任务。