1.串行的WEB服务器
class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true) {
Socket connection = socket.accept();
handleRequest(connection);
}
}
}
2.为任务创建线程
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();
}
}
}
无限制创建线程的不足
- 线程生命周期的开销非常高。线程的创建与销毁并不是没有代价的。根据平台的不同,实际的开销也有所不同。
如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况那么为每个
请求创建一个新线程将消耗大量的计算资源。 - 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有
些线程将闲置大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时还将
产生其它的性能开销。如果你已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更多的线程反而
会降低性能。 - 稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,
包括JVM的启动参数、Thread构造函数中请求的栈大小,以及低层操作系统对线程的限制等。 - “为每个任务分配一个线程”这种方法的问题在于,它没有限制可创建线程的数量。在32位的机器上,其中一个
主要的限制因素是线程栈的地址空间。每个线程都要维护两个执行栈,一个用于JAVA代码,另一个用于原生
代码。通常,JMV在默认情况下会生成一个复合的栈,大小约为0.5MB。(可以通过JVM标志-Xss或者通过
Thread的构造函数来修改这个值)。如果将2的32次方除以每个线程的栈大小,那么线程数量将被限制为几
千到几万。
Executor框架
在JAVA类库中,任务执行的主要抽象不是THREAD,而是Executor。
public interface Executor {
void execute(Runnale command);
}
Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。如果要在程序中
实现一个生产者-消费者的设计,那么最简单的方式通常就是使用Executor。
基于Executor的web服务器
class TaskExecutionWebServer {
private static final int NTHREADS = 100;
private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);
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);
}
};
exec.execute(task);
}
}
}
种不同的Executor实现,就可以改变服务器的行为。
通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和修改执行策略。各种执行策略都是一种
资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。
每当看到下面这种形式的代码时:
new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread。
线程池
类库提供了一个灵活的线程池以及一些有用的默认配置可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
- newFixedThreadPool
- newCachedThreadPool
- newSingleThreadExecutor
- newScheduledThreadPool
从“为每个任务分配一个线程”策略变成基于线程池的策略,将对应用程序的稳定性产生重大的影响:WEB服务器不会再在高负载情况下
失败(尽管服务器不会因为创建了过多 的线程而失败,但在足够长的时间内,如果途程到达的速度总是超过任务执行的速度,那么服务器
仍有可能耗尽内存,因为等待执行的Runnable队列将不断增长。可以通过使用一个有界队列在Executor框架内部解决这个问题)。由于
服务器不会创建数千个线程来争夺有限的CPU和内存资源,因此服务器的性能将平缓的降低。通过使用Executor,可以实现各种调优、
管理、监视、记录日志、错误报告和其它功能。
为了解决执行服务的生命周期问题,ExecutorService扩展了Exceutor接口,添加了一些用于生命周期管理的方法。
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutDown();
boolean isTerminated();
boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedException;
}
支持关闭操作的WEB服务器
class LifecycleWebServer {
private final ExecutorService exec = ...;
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while(!exec.isShutdown()) {
try{
final Socket conn = socket.accept();
exec.execute(new Runnable(){
public void run() {
//handleRequest(conn);
}
});
} catch(RejectedExecutionException e) {
if(!exec.isShutdown()) {
//log("task submission rejected",e);
}
}
}
}
public void stop() {
exec.shutdown();
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if(isShutdownRequest(req)) {
stop();
} else {
dispatchRequest(req);
}
}
}