大多数的并发应用程序都是围绕”任务执行”来构造的:任务通常是一些离散的单元。所谓高并发的主要任务就是把应用程序中的工作分解到多个任务当中,并且这些任务是相互独立的:任务不依赖于其他任务的状态。任务之间的独立性有助于实现并发,只要存在足够多的资源,那么这些独立的任务都可以并行执行。
现在通常情况下,我们对执行任务有三种解决方法。
- 串行地执行任务
- 显式地为任务创建线程
- 线程池
下面就先主要分析前两种方式的原理与不足之处:
串行地执行任务
在应用程序中可以通过多种策略来调度任务,其中的一些策略可以很好地利用潜在的并发性。
最简单的策略就是在单个线程中串行地执行各项任务。
public static void main(String[] args){
try {
ServerSocket socket = new ServerSocket(80);
while (true){
Socket connection = socket.accept();
handleRequest(connection);
}
} catch (IOException e) {
e.printStackTrace();
}
}
这里对任务的处理十分简单。在主线程当中依次处理到来的任务请求。但是实际性能很糟糕,因为每次只能处理一个请求,主线程在接受连接与处理相关请求等操作之间不能交替运行,也就是说必须处理完本次接受的任务服务器才能进行下一次任务的accept。
在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。
显式地为任务创建线程
这种方法通常为每个请求创建一个新的线程来提供服务,从而实现高的响应性。
public static void main(String[] args){
try {
ServerSocket socket = new ServerSocket(80);
while (true){
final Socket connection = socket.accept();
Runnable task = new Runnable() {
@Override
public void run() {
handleReuqest(connection);
}
};
new Thread(task).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
这种方式与串行机制的区别在于对于每个请求的连接,主循环都会创建一个新线程来处理请求,而不是在主线程当中进行任务处理。
- 任务处理过程从主线程中分离出来,使得主线程更快的监听下一个请求的到来,这使得程序在完成前一个任务之前可以接受新的任务请求,从而提高响应性。
- 任务可以并行处理,从而能同时服务多个请求
- 任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。
在正常负载的情况下,为每一个请求分配一个线程能提升串行执行的性能。只要请求的到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。
无限制创建线程的不足
在有大量任务请求到来时,这种方法存在一些缺陷:
- 线程生命周期的开销非常高:线程的创建与销毁不是没有代价的。线程的创建过程都会需要时间,延迟处理的请求。如果请求的到达率非常高且请求的处理过程是轻量级的,那么为每个请求创建一个新的线程将消耗大量的计算资源。
- 资源消耗:活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程多于可用处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源还将产生其他的性能开销。
- 稳定性:在可创建线程的数量上存在一个限制。如果破坏了这个限制,很可能抛出OOM异常。