3.6 创建多线程的服务器
在本书第1章的1.5.1节的例程1-2的EchoServer中,其service()方法负责接收客户连接,以及与客户通信。service()方法的处理流程如下:
while (true) { Socket socket=null; try { socket = serverSocket.accept(); //接收客户连接 //从Socket中获得输入流与输出流,与客户通信 … }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); //断开连接 }catch (IOException e) {e.printStackTrace();} } } |
EchoServer接收到一个客户连接,就与客户进行通信,通信完毕后断开连接,然后再接收下一个客户连接。假如同时有多个客户请求连接,这些客户就必须排队等候EchoServer的响应。EchoServer无法同时与多个客户通信。
许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。
可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:
◆能同时接收并处理多个客户连接;
◆对于每个客户,都会迅速给予响应。
服务器同时处理的客户连接数目越多,并且对每个客户作出响应的速度越快,就表明并发性能越高。
用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。本节将按照3种方式来重新实现EchoServer,它们都使用了多线程。
◆为每个客户分配一个工作线程。
◆创建一个线程池,由其中的工作线程来为客户服务。
◆利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。
3.6.1 为每个客户分配一个线程
服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。以下是EchoServer的service()方法的代码:
public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //接收客户连接 Thread workThread=new Thread(new Handler(socket)); //创建一个工作线程 workThread.start(); //启动工作线程 }catch (IOException e) { e.printStackTrace(); } } } |
以上工作线程workThread执行Handler的run()方法。Handler类实现了Runnable接口,它的run()方法负责与单 个客户通信,与客户通信结束后,就会断开连接,执行Handler的run()方法的工作线程也会自然终止。如例程3-5所示是EchoServer类及 Handler类的源程序。
例程3-5 EchoServer.java(为每个任务分配一个线程)
package multithread1; import java.io.*; import java.net.*; public class EchoServer { private int port=8000; private ServerSocket serverSocket; public EchoServer() throws IOException { serverSocket = new ServerSocket(port); System.out.println("服务器启动"); } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //接收客户连接 Thread workThread=new Thread(new Handler(socket)); //创建一个工作线程 workThread.start(); //启动工作线程 }catch (IOException e) { e.printStackTrace(); } } } public static void main(String args[])throws IOException { new EchoServer().service(); } } class Handler implements Runnable{ //负责与单个客户的通信 private Socket socket; public Handler(Socket socket){ this.socket=socket; } private PrintWriter getWriter(Socket socket)throws IOException{…} private BufferedReader getReader(Socket socket)throws IOException{…} public String echo(String msg) {…} public void run(){ try { System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); BufferedReader br =getReader(socket); PrintWriter pw = getWriter(socket); String msg = null; while ((msg = br.readLine()) != null) { //接收和发送数据,直到通信结束 System.out.println(msg); pw.println(echo(msg)); if (msg.equals("bye")) break; } }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); //断开连接 }catch (IOException e) {e.printStackTrace();} } } } |
3.6.2 创建线程池
在3.6.1节介绍的实现方式中,对每个客户都分配一个新的工作线程。当工作线程与客户通信结束,这个线程就被销毁。这种实现方式有以下不足之处。
◆服务器创建和销毁工作线程的开销(包括所花费的时间和系统资源)很大。如果服务器需要与许多客户通信,并且与每个客户的通信时间都很短,那么有可能服务器为客户创建新线程的开销比实际与客户通信的开销还要大。
◆除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本身都会占用一定的内存(每个线程需要大约1M内存),如果同时有大量客户连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致系统的内存空间不足。
◆如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是相对固定的。不同操作系统有不同的切换周期,一般在20毫秒左右。这里所说 的线程切换是指在Java虚拟机,以及底层操作系统的调度下,线程之间转让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,因为一个 线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切换周期,切换线程的 开销甚至比创建及销毁线程的开销还大。
线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下优点:
◆减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务。
◆可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃。
如例程3-6所示,ThreadPool类提供了线程池的一种实现方案。
例程3-6 ThreadPool.java
package multithread2; import java.util.LinkedList; public class ThreadPool extends ThreadGroup { private boolean isClosed=false; //线程池是否关闭 private LinkedList workQueue; //表示工作队列 private static int threadPoolID; //表示线程池ID private int threadID; //表示工作线程ID public ThreadPool(int poolSize) { //poolSize指定线程池中的工作线程数目 super("ThreadPool-" + (threadPoolID++)); setDaemon(true); workQueue = new LinkedList(); //创建工作队列 for (int i=0; i new WorkThread().start(); //创建并启动工作线程 } /** 向工作队列中加入一个新任务,由工作线程去执行该任务 */ public synchronized void execute(Runnable task) { if (isClosed) { //线程池被关则抛出 IllegalStateException异常 throw new IllegalStateException(); } if (task != null) { workQueue.add(task); notify(); //唤醒正在getTask()方法中等待任务的工作线程 } } /** 从工作队列中取出一个任务,工作线程会调用此方法 */ protected synchronized Runnable getTask()throws InterruptedException{ while (workQueue.size() == 0) { if (isClosed) return null; wait(); //如果工作队列中没有任务,就等待任务 } return workQueue.removeFirst(); } /** 关闭线程池 */ public synchronized void close() { if (!isClosed) { isClosed = true; workQueue.clear(); //清空工作队列 interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类 } } /** 等待工作线程把所有任务执行完 */ public void join() { synchronized (this) { isClosed = true; notifyAll(); //唤醒还在getTask()方法中等待任务 的工作线程 } Thread[] threads = new Thread[activeCount()]; //enumerate()方法继承自ThreadGroup类,获得线程组中当前所有活着的工作线程 int count = enumerate(threads); for (int i=0; i try { threads[i].join(); //等待工作线程运行结束 }catch(InterruptedException ex) { } } } /** 内部类:工作线程 */ private class WorkThread extends Thread { public WorkThread() { //加入到当前ThreadPool线程组中 super(ThreadPool.this,"WorkThread-" + (threadID++)); } public void run() { while (!isInterrupted()) { //isInterrupted()方法继承自Thread类,判断线程是否被中断 Runnable task = null; try { //取出任务 task = getTask(); }catch (InterruptedException ex){} // 如果getTask()返回null或者线程执行getTask()时被中断,则结束此线程 if (task == null) return; try { //运行任务,异常在catch代码块中捕获 task.run(); } catch (Throwable t) { t.printStackTrace(); } } //#while } //#run() } //#WorkThread类 } |
在ThreadPool类中定义了一个LinkedList类型的workQueue成员变量,它表示工作队列,用来存放线程池要执行的任务,每个 任务都是Runnable实例。ThreadPool类的客户程序(利用ThreadPool来执行任务的程序)只要调用ThreadPool类的 execute (Runnable task)方法,就能向线程池提交任务。在ThreadPool类的execute()方法中,先判断线程池是否已 经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加入到工作队列中,并且唤醒正在等待任务的工作线程。