6.1 在线程中执行任务
清晰的任务边界
在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态,结果或边界效应。
独立性有助于实现并发。
大多数服务器应用程序都提供了一种自然的任务边界选择方式,以独立的客户请求为边界。
6.1.1 串行地执行任务(Executing Tasks Sequentially【继续地】)
在应用程序中可以通过多种策略来调度任务,而其中一些策略能够更好地利用潜在并发性。
最简单的策略就是在单个线程中串行地执行各项任务。
SingleThreadWebServer将串行地处理它的任务(通过80端口接收到HTTP请求)。
// 6-1 串行的Web服务器(并不好)
public class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) { //处理多个请求
Socket connection = socket.accept();//通过80端口接收到HTTP请求
handleRequest(connection); //处理请求
}
}
private static void handleRequest(Socket connection) {
// 请求处理
}
}
通过80端口接收到HTTP请求很简单且理论上是正确的,但在实际生产环境中的执行性能很糟糕,因为它每次只能处理一个请求。主线程在接受连接与处理相关请求等操作之间不断交替运行。
在Web请求的处理中包含了一组不同的运算与I/O操作。服务器必须处理套接字I/O以读取请求和写回响应,这些操作通常会由于网络阻塞或连通性问题而被阻塞。
在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。
6.1.2 显式地为任务创建线程(Explicitly 【明确地】Creating Threads for Tasks)
通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。
// 6-2 在Web服务器中为每个请求启动一个新的线程(不要这么做)
public class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) { //多个请求
final Socket connection = socket.accept();
Runnable task = new Runnable() { //为每个请求启动一个新的线程
public void run() {
handleRequest(connection);
}
};
new Thread(task).start();//线程启动
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
ThreadPerTaskWebServer的结构与串行版本类似——主线程仍然不断地交替执行“接受外部请求”与“分发请求”等操作。区别在于,对于每个连接,主循环都创建一个新线程来处理请求,而不是在主循环中处理。
由此得出3个主要结论:
①任务处理过程从主线程中分离出来,使得主循环能够更快地等待下个到来的连接。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
②任务可以并行处理 ,从而同时服务多个请求。如果有多个处理器,或者任务由于某个原因被阻塞,例如等待I/O完成,获取锁或者资源可用性等,程序的吞吐率将得到提高。
③任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。
正常情况下,“为每个任务分配一个线程”能提升串行执行的性能。只要请求的到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。
6.1.3 无限制创建线程(Unbounded Thread Creation)的不足
“为每个任务分配一个线程”存在一些缺陷,特别是当需要创建大量的线程时:
①线程生命周期的开销非常高。
②资源消耗。
活跃的线程会消耗系统资源,尤其是内存。
③稳定性
在可创建线程的数量上存在一个限制。
在一定的范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,在创建更多的线程只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。
要想避免这种危险,就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。
“为每个任务分配一个线程”问题在于,没有限制可创建线程的数量,只限制了远程用户提交HTTP请求的速率。
6.2 Executor框架(Framework)
任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。
把所有任务放在单个线程中,以及将每个任务放在各自的线程中执行,这两种方式都存在一些严格限制:串行执行的问题在于其糟糕的响应性和吞吐率,而“为每个任务分配一个线程”问题在于资源管理的复杂性。
线程池简化了线程的管理任务,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。
// Excutor接口
public interface Executor{
void execute(Runnable command);
}
Executor是个简单的接口,为灵活且强大的异步执行任务框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。
Executor基于生产者-消费者模式,提交任务的操作相当于生成者(生成待完成的工作单元),执行任务的线程则相当于消费者(执行完这些工作单元)。
如果要在程序中实现一个生产者-消费者的设计,那么最简答的方式就是使用Executor。(而我们之前的章节中使用的是阻塞队列来实现的,利用其take和put方法)
6.2.1 基于Executor的Web服务器
TaskExecutionWebServer 代替了硬编码的线程创建过程。
使用了标准的Executor实现,即一个固定长度的线程池,可以容纳100个线程
// 6-4 基于线程池的Web服务器
public class TaskExecutionWebServer {
private static final int NTHREADS=100;
private static final Executor exec=
Executors.newFixedThreadPool(NTHREADS); //创建了一个固定长度的线程池,可以容纳100个线程
public static void main(String[] args)throws IOException{
ServerSocket socket=new ServerSocket(80);//创建一个Server Socket绑定到80端口自
while(true){
final Socket connection=socket.accept();
Runnable task=new Runnable(){
public void run(){
handleRequest(connection);
}
};
exec.execute(task);//将任务提交到工作队列中,在某个时刻被工作线程取出并执行
}
}
private static void handleRequest(Socket connection) {
//处理请求
}
}
通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开来,并且只需采用另一种不同的Executor实现,就可以改变服务器的行为。
通常,Executor的配置是一次性,因此可以在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个程序中,增加了修改的难度。
我们可以将TaskExecutionWebServer修改为类似ThreadPerTaskWebServer的行为,只需使用一个为每个请求都创建新线程的Executor。
// 6-5 为每个请求启动一个新线程的Executor
public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
};
}
同样