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()方法中,先判断线程池是否已
经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加入到工作队列中,并且唤醒正在等待任务的工作线程。
相关推荐
《Java2编程详解》是一本全面深入探讨Java编程技术的书籍,主要针对Java 2平台进行讲解。在Java 2平台上,Java语言的功能得到了极大的扩展,包括多线程、网络编程、I/O流、数据库连接、图形用户界面(GUI)以及Java...
本教程将深入探讨Java在网络编程方面的基本概念和用法。 1. **Java网络编程基础** - Java网络编程主要依赖于Java的`java.net`和`java.io`包,它们提供了丰富的类和接口来处理网络连接。 - `Socket`和`...
《Java2编程详解》这本书是Java编程领域的一部经典之作,特别版的使用指南深入剖析了Java 2平台的核心技术和编程方法。这本书旨在帮助开发者掌握Java语言的基础以及高级特性,从而能够有效地进行软件开发。 首先,...
总之,《Java2编程详解》是一本全面覆盖Java编程的指南,不仅包含基本语法和面向对象设计,还深入讲解了高级主题如并发、网络和反射,对于任何想要深入学习Java的人来说都是一份宝贵的资源。通过学习本书,读者将...
【Java编程思想详解】 Java编程思想是理解和使用Java语言的核心理念,它不仅仅是关于语言特性的简单堆砌,而是一种设计理念,强调的是设计、抽象和问题解决。Java的强大在于其面向对象的设计,它允许开发者以更加...
《Java2编程详解(Special Edition Using Java)》是一本针对Java初学者和爱好者精心编写的教程,旨在提供全面且深入的Java编程知识。本书详细介绍了Java语言的核心概念、语法和应用,是学习Java 2平台的理想资源。...
Java编程详解是一个深入探讨Java语言及其应用的领域,特别是针对最新的Java版本。...通过阅读《Java 2编程详解Special Edition Using Java2》这样的资源,开发者可以深入了解Java编程的各个方面,提升自己的专业技能。
Java网络socket编程是Java编程语言在网络编程中的一项核心技术,主要涉及到网络通信的两个基本概念:客户端(Client)和服务器(Server)。在Java中实现网络通信,主要依赖于Socket编程模型,尤其是基于TCP/IP协议的...
Java基础入门编程详解,这本书籍是为初学者精心编写的,旨在帮助他们系统地学习Java编程语言的基础知识。Java作为一种广泛应用于互联网、企业级应用、移动开发等领域的编程语言,其扎实的基础对于开发者来说至关重要...
在传统的Java网络编程中,我们通常使用`Socket`类进行网络通信。当服务器接收到客户端的连接请求后,会为每个客户端创建一个新的线程来处理其请求。例如,下面的代码片段展示了如何使用阻塞方式接收客户端的数据: ...
#### 二、Java Socket编程详解 1. **TCP Socket编程**: - **创建Socket连接**:客户端通过`Socket`构造方法创建一个到服务器的连接。 - **创建ServerSocket**:服务器端通过`ServerSocket`监听指定端口上的连接...
例如,Java作为一种强大的编程语言,如何通过SOCKET来实现网络通信,以及APPELT(虽然现在已经较少使用)如何嵌入到网页中并通过网络与用户进行交互。 #### 思路详解 实训项目主要包括以下几个方面: 1. **设计三...
5. **API详解**:Java的`java.net`包提供了这些网络编程相关的类和接口。例如,Socket和ServerSocket在`java.net`包下,而InetAddress类用于处理IP地址,URL类用于表示统一资源定位符,这些都与网络通信密切相关。 ...
根据提供的信息,《java编程详解》是一本被广泛推荐并深受读者喜爱的专业书籍,它旨在为初学者和进阶学习者提供全面、深入的Java编程知识。以下是对该书可能涵盖的一些核心知识点的概述: ### Java语言基础 1. **...
Java网络编程的核心在于客户端与服务器端的交互,而ServerSocket是Java中用于服务器端的类,它使得服务器能够监听特定端口,接收客户端的连接请求。本文将详细讲解ServerSocket的使用方法及其在多线程环境下的应用。...
### Java使用Socket网络编程详解 #### 一、引言 Socket是网络通信的基础,它使得不同计算机间的进程可以通过网络进行通信。本篇文章旨在详细介绍如何在Java中使用Socket进行网络编程,特别是TCP协议下的客户端与...