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()方法中,先判断线程池是否已
经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加入到工作队列中,并且唤醒正在等待任务的工作线程。
相关推荐
Java网络编程基础用法详解.rar
java 编程 主要函数 用法详解 概括了主要函数和常用函数的用法。真的很好,欢迎大家下载。
在客户/ 服务器通信模式中, 服务器端需要创建监听特定端口的 ServerSocket , ServerSocket 负责接收客户连接请求。...章还介绍了 java.util.concurrent 包中的线程池类的用法,在服务器程序中可以直接使用它们。
5. 2 Java网络编程中的高层类 5. 2. 1 URL类 5. 2. 2 URLConnection类 第6章 利用Servlet上传和下载文件 6. 1 得到HTTP请求消息的内容 6. 2 利用Servlet得到上传的文件 6. 2. 1 上传文件及表单域的请求实体的...
Java以其简单、面向对象...本书包含了Java情况2语言参考、Java 2 API参考和Java 2编程指南等主要内容,详细介绍了Java情况2的核心技术、Java 2 API类和接口的用法。无论是初学者,还是Java高手,本书都会让你获益匪浅。
031901_【第19章:Java网络编程】_IP(Internet Protocol)与InetAddress笔记.pdf 031902_【第19章:Java网络编程】_URL与URLConnection笔记.pdf 031903_【第19章:Java网络编程】_URLEncoder与URLDecoder笔记.pdf ...
数据库编程知识值得你拥有 包含数据库基本语句及其使用方法
Java网络编程的基础知识、套接字编程、非阻塞通信、创建HTTP服务器与客户程序、数据报通信、对象的序列化与反序列化、Java反射机制、RMI框架、JDBC API、JavaMail API、MVC设计模式、安全网络通信、CORBA和Web服务
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
北京动力节点-Java编程零基础教程-107-Java基本语法-方法初步-使用方法与不使用方法的对比.avi 北京动力节点-Java编程零基础教程-108-Java基本语法-方法初步-方法有返回值与无返回值的对比.avi 北京动力节点-Java...
本书讲述在因特网中占主流地位的编程语言--Java...详细讲解Java API中类和接口的使用方法。在附录取中对Java 2中的新增的Swing做了简要介绍。 本书可作Java程序员日常的编程参考手册,适合计算机、网络技术人员使用。
像这样的日志代码会更好: if (log.isLoggable(Level.FINE)) { ...关于这个主题有大量优秀的资源,相关的方法和工具也不只针对Java。假定你已经完成了分析,并且判断出是运行环境中Java 组件的性能需要改善。
本文将深入探讨Java多线程编程的重要性和使用方法。介绍多线程概念,讨论多线程的优势,并提供实际示例。此外,还将探讨多线程编程中的常见问题以及如何避免这些问题。通过本文,您将获得对Java多线程编程的全面理解...
1.2.3 Java编程语言的特点 8 1.3 Java语言的开发环境 8 1.4 搭建Java开发环境 9 1.4.1 安装与设置JDK 9 1.4.2 安装与启动Eclipse IDE 12 习题 15 第2章 构建Java应用程序 16 2.1 如何构建应用程序 ...
JTable是Swing编程中很常用的控件,这里总结了一些常用方法以备查阅.希望对大家有所帮助!!
第二部分引入了ClassLoader,这是因为ClassLoader与线程不无关系,我们可以通过synchronized关键字,或者Lock等显式锁的方式在代码的编写阶段对共享资源进行数据一致性保护,那么一个Class在完成初始化的整个过程到...
本文档主要讲述的是java表格控件JTable常用操作详解;JTable是Swing编程中很常用的控件,文中总结了一些常用方法以备查阅。
Stream是 Java 8新增加的类,用来补充集合类。 Stream代表数据流,流中的数据元素的数量... Java Stream提供了提供了串行和并行两种类型的流,保持一致的接口,提供函数式编程方式,以管道方式提供中间操作和终执
Object是Java编程中的基础类,所有类都直接或间接地继承了它,并从它继承了一些方法,如equals()、toString()和getClass()...熟练掌握和灵活使用Object类和其相关的方法能够帮助开发人员更好地理解和使用Java编程语言。
《Java线程 高清晰中文第二版》中文第二版(PDF) 前言 第一章 线程简介 Java术语 线程概述 为什么要使用线程? 总结 第二章 Java线程API 通过Thread类创建线程 使用Runable接口的线程 线程的生命周期 线程命名 ...