多任务处理:线程池
线程池
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
与一客户一线程服务器一样,线程池服务器首先创建一个ServerSocket实例。然后创建N个线程,每个线程都反复循环,从(共享的)ServerSocket实例接收客户端连接。当多个线程同时调用同一个ServerSocket实例的accept()方法时,它们都将阻塞等待,直到一个新的连接成功建立。然后系统选择一个线程,新建立的连接对应的Socket实例则只在选中的线程中返回。其他线程则继续阻塞,直到成功建立下一个连接和选中另一个幸运的线程。
由于线程池中的所有线程都反复循环,一个接一个地处理客户端连接,线程池服务器的行为就像是一组迭代服务器。与一客户一线程服务器不同,线程池中的线程在完成对一个客户端的服务后并不终止,相反,它又重新开始在accept()方法上阻塞等待。TCPEchoServerPool.java中演示了一个线程池的例子。
TCPEchoServerPool.java
0 import java.io.IOException;
1 import java.net.ServerSocket;
2 import java.net.Socket;
3 import java.util.logging.Level;
4 import java.util.logging.Logger;
5
6 public class TCPEchoServerPool {
7
8 public static void main(String[] args) throws
IOException {
9
10 if (args.length != 2) { // Test for correct # of args
11 throw new IllegalArgumentException("Parameter(s):
<Port> <Threads>");
12 }
13
14 int echoServPort = Integer.parseInt(args[0]); // Server
port
15 int threadPoolSize = Integer.parseInt(args[1]);
16
17 // Create a server socket to accept client connection
requests
18 final ServerSocket servSock = new
ServerSocket(echoServPort);
19
20 final Logger logger = Logger.getLogger("practical");
21
22 // Spawn a fixed number of threads to service clients
23 for (int i = 0; i < threadPoolSize; i++) {
24 Thread thread = new Thread() {
25 public void run() {
26 while (true) {
27 try {
28 Socket clntSock = servSock.accept(); // Wait for a
connection
29 EchoProtocol.handleEchoClient(clntSock, logger); //
Handle it
30 } catch (IOException ex) {
31 logger.log(Level.WARNING, "Client accept failed", ex);
32 }
33 }
34 }
35 };
36 thread.start();
37 logger.info("Created and started Thread = " +
thread.getName());
38 }
39 }
40 }
TCPEchoServerPool.java
1.设置:第10-20行
要侦听的端口号和线程的数量都作为参数传递给main()。对参数进行解析后再创建
ServerSocket 和Logger实例。注意要它们都必须声明为常量(final),因为它们将在下面创
建的匿名类中引用。
2.创建并启动threadPoolSize个新线程:第23-38行
循环的每一次迭代都会创建一个继承于Thread的匿名类的实例。当调用该实例的start()方法时,这个线程就会执行该匿名类的run()方法。run()方法将反复循环,接受客户端的连接请求,并传递给EchoProtocol进行处理。
接受连接请求:第28行
由于有N个不同线程在执行同一个循环,那么最多有N个线程在servSock的accept()方法上阻塞等待传入的连接请求。对于任何一个连接,系统保证了只要一个线程能够获得其对应的Socket。在一个客户端连接被创建时,如果没有线程在accept()方法上阻塞等待(即,所有线程都在忙着为其他连接服务),系统则将新的连接排列在一个队列中,直到下一次调用accept()方法(见第6.4.1节)。
将客户端套接字传递给EchoProtocol.handleEchoClient()方法:第29行
handleEchoClient()方法中封装了协议的详细内容。该方法在连接处理完成后将相关信息写入日志,处理过程中遇到的异常也将写入日志。
处理accept()方法抛出的异常:第31行
由于线程的重复使用,线程池的方法只需要付出创建N次线程的系统开销,而与客户端连接总数无关。由于可以控制最大并发执行线程数,我们就可以控制线程的调度和资源开销。当然,如果我们创建的线程太少,客户端还是有可能等很长时间才获得服务,因此,线程池的大小需要根据负载情况进行调整,以使客户端连接的时间最短。理想的情况是有一个调度工具,可以在系统负载增加时扩展线程池的大小(低于大小上限),负载较轻时缩减线程池的大小。Java恰好就有这种工具,我们将在下一节进行介绍。
相关下载:
Java_TCPIP_Socket编程(doc)
http://download.csdn.net/detail/undoner/4940239
文献来源:
UNDONER(小杰博客) :http://blog.csdn.net/undoner
LSOFT.CN(琅软中国) :http://www.lsoft.cn