多任务处理:线程池

线程池

 每个新线程都会消耗系统资源:创建一个线程将占用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个线程在servSockaccept()方法上阻塞等待传入的连接请求。对于任何一个连接,系统保证了只要一个线程能够获得其对应的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

posted on 2012-12-23 09:44  吴一达  阅读(336)  评论(0编辑  收藏  举报

导航