Java 网络编程 —— 创建多线程服务器
一个典型的单线程服务器示例如下:
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();
}
}
}
}
服务端接收到一个客户连接,就与客户进行通信,通信完毕后断开连接,然后接收下一个客户连接,假如同时有多个客户连接请求这些客户就必须排队等候。如果长时间让客户等待,就会使网站失去信誉,从而降低访问量。
一般用并发性能来衡量一个服务器同时响应多个客户的能力,一个具有好的并发性能的服务器,必须符合两个条件:
- 能同时接收并处理多个客户连接
- 对于每个客户,都会迅速给予响应
用多个线程来同时为多个客户提供服务,这是提高服务器并发性能的最常用的手段,一般有三种方式:
- 为每个客户分配一个工作线程
- 创建一个线程池,由其中的工作线程来为客户服务
- 利用 Java 类库中现成的线程池,由它的工作线程来为客户服务
为每个客户分配一个线程
服务器的主线程负责接收客户的连接,每次接收到一个客户连接,都会创建一个工作线程,由它负责与客户的通信
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 TOException {
new EchoServer().service();
}
// 负责与单个客户的通信
class Handler implements Runnable {
private Socket socket;
pub1ic 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("from "+ socket.getInetAddress() + ":" + socket.getPort() + ">" + msg);
pw.println(echo(msg));
if (msg.equals("bye")) break;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 断开连接
if(socket != nulll) socket.close();
} catch (IOException e) {
e,printStackTrace();
}
}
}
}
}
创建线程池
上一种实现方式有以下不足之处:
- 服务器创建和销毁工作线程的开销很大,如果服务器需要与许多客户通信,并且与每个客户的通信时间都很短,那么有可能服务器为客户创建新线程的开销比实际与客户通信的开销还要大
- 除了创建和销毁线程的开销,活动的线程也消耗系统资源。每个线程都会占用一定的内存,如果同时有大量客户连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致系统的内存空间不足
线程池中预先创建了一些工作线程,它们不断地从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务,就会继续执行工作队列中的下一个任务
线程池具有以下优点:
- 减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务
- 可以根据系统的承载能力,方便调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃
public class ThreadPool extends ThreadGroup {
// 线程池是否关闭
private boolean isClosed = false;
// 表示工作队列
private LinkedList<Runnable> workQueue;
// 表示线程池ID
private static int threadPoolID;
// 表示工作线程ID
// poolSize 指定线程池中的工作线程数目
public ThreadPool(int poolSize) {
super("ThreadPool-"+ (threadPoolID++));
setDaemon(true);
// 创建工作队列
workQueue = new LinkedList<Runnable>();
for (int i = 0; i < poolSize; i++) {
// 创建并启动工作线程
new WorkThread().start();
}
}
/**
* 向工作队列中加入一个新任务,由工作线程去执行任务
*/
public synchronized void execute(Runnable tank) {
// 线程池被关则抛出IllegalStateException异常
if(isClosed) {
throw new IllegalStateException();
}
if(task != null) {
workQueue.add(task);
// 唤醒正在getTask()方法中等待任务的工作线限
notify();
}
}
/**
* 从工作队列中取出一个任务,工作线程会调用此方法
*/
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();
// 中断所有的工作线程,该方法继承自ThreadGroup类
interrupt();
}
}
/**
* 等待工作线程把所有任务执行完
*/
public void join() {
synchronized (this) {
isClosed = true;
// 唤醒还在getTask()方法中等待任务的工作线程
notifyAll();
}
Thread[] threads = new Thread[activeCount()];
// enumerate()方法继承自ThreadGroup类获得线程组中当前所有活着的工作线程
int count = enumerate(threads);
// 等待所有工作线程运行结束
for(int i = 0; i < count; i++) {
try {
// 等待工作线程运行结束
threads[i].join();
} catch((InterruptedException ex) {}
}
}
/**
* 内部类:工作线程
*/
private class WorkThread extends Thread {
public WorkThread() {
// 加入当前 ThreadPool 线程组
super(ThreadPool.this, "WorkThread-" + (threadID++));
}
public void run() {
// isInterrupted()方法承自Thread类,判断线程是否被中断
while (!isInterrupted()) {
Runnable task = null;
try {
// 取出任务
task = getTask();
} catch(InterruptedException ex) {}
// 如果 getTask() 返回 nu11 或者线程执行 getTask() 时被中断,则结束此线程
if(task != null) return;
// 运行任务,异常在catch代码块中被捕获
try {
task.run();
} catch(Throwable t) {
t.printStackTrace();
}
}
}
}
}
使用线程池实现的服务器如下:
publlc class EchoServer {
private int port = 8000;
private ServerSocket serverSocket;
private ThreadPool threadPool; // 线程港
private final int POOL_SIZE = 4; // 单个CPU时线程池中工作线程的数目
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
// 创建线程池
// Runtime 的 availableProcessors() 方法返回当前系统的CPU的数目
// 系统的CPU越多,线程池中工作线程的数目也越多
threadPool= new ThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_SIZE);
System.out.println("服务器启动");
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
// 把与客户通信的任务交给线程池
threadPool.execute(new Handler(socket));
} catch(IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws TOException {
new EchoServer().service();
}
// 负责与单个客户的通信,与上例类似
class Handler implements Runnable {...}
}
使用 Java 提供的线程池
java.util.concurrent 包提供了现成的线程池的实现,更加健壮,功能也更强大,更多关于线程池的介绍可以这篇文章:
public class Echoserver {
private int port = 8000;
private ServerSocket serverSocket;
// 线程池
private ExecutorService executorService;
// 单个CPU时线程池中工作线程的数目
private final int POOL_SIZE = 4;
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
// 创建线程池
// Runtime 的 availableProcessors() 方法返回当前系统的CPU的数目
// 系统的CPU越多,线程池中工作线程的数目也越多
executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_SIZE);
System.out.println("服务器启动");
}
public void service() {
while(true) {
Socket socket = null;
try {
socket = serverSocket.accept();
executorService.execute(new Handler(socket));
} catch(IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws TOException {
new EchoServer().service();
}
// 负责与单个客户的通信,与上例类似
class Handler implements Runnable {...}
}
使用线程池的注意事项
虽然线程池能大大提高服务器的并发性能,但使用它也存在一定风险,容易引发下面的问题:
死锁
任何多线程应用程序都有死锁风险。造成死锁的最简单的情形是:线程 A 持有对象 X 的锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的锁,并且在等待对象 X 的锁,线程 A 与线程 B 都不释放自己持有的锁,并且等待对方的锁,这就导致两个线程永远等待下去,死锁就这样产生了
任何多线程程序都有死锁的风险,但线程池还会导致另外一种死锁:假定线程池中的所有工作线程都在执行各自任务时被阻塞,它们都在等待某个任务 A 的执行结果。而任务 A 依然在工作队列中,由于没有空闲线程,使得任务 A 一直不能被执行。这使得线程池中的所有工作线程都永远阻塞下去,死锁就这样产生了
系统资源不足
如果线程池中的线程数目非常多,这些线程就会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能
并发错误
线程池的工作队列依靠
wait()和notify()方法来使工作线程及时取得任务,但这两个方法都难以使用。如果编码不正确,就可能会丢失通知,导致工作线程一直保持空闲状态,无视工作队列中需要处理的任务线程泄漏
对于工作线程数目固定的线程池,如果工作线程在执行任务时抛出 RuntimeException 或 Error,并且这些异常或错误没有被捕获,那么这个工作线程就会异常终止,使得线程池永久地失去了一个工作线程。如果所有的工作线程都异常终止,线程池变为空,没有任何可用的工作线程来处理任务
导致线程泄漏的另一种情形是,工作线程在执行一个任务时被阻塞,比如等待用户的输入数据,但是由于用户一直不输入数据(可能是因为用户走开了),导致这个工作线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。假如线程池中所有的工作线程都处于这样的阻塞状态,那么线程池就无法处理新加入的任务了
任务过载
当工作队列中有大量排队等候执行的任务,这些任务本身可能会消耗太多的系统资源而引起系统资源缺乏
综上所述,线程池可能会带来种种风险,为了尽可能避免它们,使用线程池时需要遵循以下原则:
如果任务 A 在执行过程中需要同步等待任务 B 的执行结果,那么任务 A 不适合加入线程池的工作队列中。如集把像任务 A 一样的需要等待其他任务执行结果的任务加入工作队列中,就可能会导致线程池的死锁
如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间避免工作线程永久地阻塞下去而导致线程泄漏
了解任务的特点,分析任务是执行经常会阻塞的 IO 操作,还是执行一直不会阻塞的运算操作。前者时断时续地占用 CPU,而后者对 CPU 具有更高的利用率。根据任务的特点,对任务进行分类,然后把不同类型的任务分别加入不同线程池的工作队列中,这样可以根据任务的特点分别调整每个线程池
调整线程池的大小,线程池的最佳大小主要取决于系统的可用 CPU 的数目以及工作队列中任务的特点。假如在一个具有 N 个 CPU 的系统上只有一个工作队列并且其中全部是运算性质的任务,那么当线程池具有 N 或 N+1 个工作线程时,一般会获得最大的 CPU 利用率
如果工作队列中包含会执行 IO 操作并经常阻塞的任务,则要让线程池的大小超过可用 CPU 的数目,因为并不是所有工作线程都一直在工作。选择一个典型的任务,然后估计在执行这个任务的过程中,等待时间(WT)与实际占用 CPU 进行运算的时间(ST)之间的比:WT/ST。对于一个具有 N 个 CPU 的系统,需要设置大约 N(1+WT/ST) 个线程来保证 CPU 得到充分利用
避免任务过载,服务器应根据系统的承受能力,限制客户的并发连接的数目。当客户的并发连接的数目超过了限制值,服务器可以拒绝连接请求,并给予客户友好提示
Java 网络编程 —— 创建多线程服务器的更多相关文章
- Java网络编程客户端和服务器通信
在java网络编程中,客户端和服务器的通信例子: 先来服务器监听的代码 package com.server; import java.io.IOException; import java.io.O ...
- Linux网络编程echo多线程服务器
echo_server服务器多线程版本 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #in ...
- Java如何创建多线程服务器?
在Java编程中,如何创建多线程服务器? 以下示例演示如何使用ServerSocket类的MultiThreadServer(socketname)方法和Socket类的ssock.accept()方 ...
- day05 Java网络编程socket 与多线程
java网络编程 java.net.Socket Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的 ...
- 网络编程 --- URLConnection --- 读取服务器的数据 --- java
使用URLConnection类获取服务器的数据 抽象类URLConnection表示一个指向指定URL资源的活动连接,它是java协议处理器机制的一部分. URL对象的openConnection( ...
- java 网络编程复习(转)
好久没有看过Java网络编程了,现在刚好公司有机会接触,顺便的拾起以前的东西 参照原博客:http://www.cnblogs.com/linzheng/archive/2011/01/23/1942 ...
- java网络编程serversocket
转载:http://www.blogjava.net/landon/archive/2013/07/24/401911.html Java网络编程精解笔记3:ServerSocket详解ServerS ...
- Java 网络编程(转)
一,网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可 ...
- 【Java】Java网络编程菜鸟进阶:TCP和套接字入门
Java网络编程菜鸟进阶:TCP和套接字入门 JDK 提供了对 TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Datagram Protoco ...
- Java 网络编程---分布式文件协同编辑器设计与实现
目录: 第一部分:Java网络编程知识 (一)简单的Http请求 一般浏览网页时,使用的时Ip地址,而IP(Internet Protocol,互联网协议)目前主要是IPv4和IPv6. IP地址是一 ...
随机推荐
- uni-app使用Sqlite
step2:封装常用操作(未对事务进行封装 HTML5+ API Reference (html5plus.org)) // //打开数据库 function openDb(name,path) { ...
- 21206134-赵景涛-第三次blog总结
一.前言: 本次Blog是对之前发布的PTA题目集的总结性Blog,这几次的作业题量,难度都不大,但都趋近于完成一整个系统,而非只实现部分的功能.题目集九.十也不在给出类图,而是要求自己设计.我认为这 ...
- 继续Vue的探索
接上集 上次到了想要利用Vue实现隔行变色的请求,但是由于使用的代码过于"高级"导致无法识别,这就需要利用webpack来解决它! webpack的基本使用 1.首先,在项目中安装 ...
- svn提交规范
本文档参考了Git提交规范,旨在规范使用SVN进行代码版本管理时的提交操作. 提交前的准备 1. 检查代码 在提交代码前,请先进行必要的代码检查,确保代码的正确性.可读性和可维护性.可以使用代码质量管 ...
- day11-MySql存储结构
MySql存储结构 参考视频:MySql存储结构 1.表空间 不同的存储引擎在磁盘文件上的结构均不一致,这里以InnoDB为例: CREATE TABLE t(id int(11)) Engine = ...
- TCC 分布式事务解决方案
更多内容,前往 IT-BLOG 一.什么是 TCC事务 TCC 是Try.Confirm.Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try.确认Confirm.撤销Canc ...
- Kafka 集群调优
更多内容,前往 IT-BLOG 单个 kafka服务器足以满足本地开发或 POC要求,使用集群的最大好处是可以跨服务器进行负载均衡,再则就是可以使用复制功能来避免因单点故障造成的数据丢失.在维护 Ka ...
- 创建镜像发布到镜像仓库【不依赖docker环境】
image 工具背景 如今,docker镜像常用于工具的分发,demo的演示,第一步就是得创建docker镜像.一般入门都会安装docker,然后用dockerFile来创建镜像,除此以外你还想过有更 ...
- 别逛了,送你一份2023年Java核心篇JVM(虚拟机)面试题整理
Java内存区域 说一下 JVM 的主要组成部分及其作用? JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载).Execution engine(执行引擎):两个组件为Ru ...
- Android APP开机启动,安卓APP开发自启动,安卓启动后APP自动启动 Android让程序开机自动运行APP 全开源下载
让APP在安卓系统启动自动运行可以带来以下几个好处:用户方便:当用户打开设备时,自动启动所需的APP可以让用户更方便地使用设备,不必手动打开APP.提高用户黏性:自动启动APP可以让用户更快地开始使用 ...