学习目标
掌握多线程服务器的架构模型,理解线程生命周期和并发基本概念
多线程服务器模型
多线程服务器采用"一个连接一个线程"的模式处理客户端请求:
→
客户端1处理线程
客户端2处理线程
客户端3处理线程
线程生命周期
新建(NEW)
→
可运行(RUNNABLE)
→
阻塞(BLOCKED)
→
等待(WAITING)
→
限时等待(TIMED_WAITING)
→
终止(TERMINATED)
并发基本概念
- 线程安全:多线程环境下程序能正确执行
- 同步:控制多个线程对共享资源的访问
- 死锁:多个线程互相等待对方释放资源
- 竞态条件:程序行为依赖于线程执行顺序
- 原子操作:不可分割的操作序列
设计原则
在设计多线程服务器时,遵循以下原则:
- 主线程只负责接受连接,不处理具体业务
- 每个客户端连接由独立线程处理
- 使用线程池管理线程资源
- 共享资源需要同步控制
- 避免线程间不必要的通信
学习目标
掌握ServerSocket的使用,实现多线程处理客户端连接
1
创建ServerSocket
创建服务器套接字并绑定端口:
import java.net.ServerSocket;
import java.net.Socket;
public class MultiThreadedServer {
private static final int PORT = 8080;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器启动,监听端口:" + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("新的客户端连接:" +
clientSocket.getInetAddress().getHostAddress());
Thread clientThread = new Thread(new ClientHandler(clientSocket));
clientThread.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
实现客户端处理器
创建ClientHandler类处理客户端请求:
import java.io.*;
import java.net.Socket;
public class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
try (
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
String response = processRequest(inputLine);
out.println(response);
}
} catch (IOException e) {
System.err.println("处理客户端请求时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String processRequest(String request) {
return "服务器响应: " + request.toUpperCase();
}
}
3
运行服务器
编译并运行服务器程序:
javac MultiThreadedServer.java
java MultiThreadedServer
服务器启动后输出:
服务器启动,监听端口:8080
4
测试客户端连接
使用telnet或nc命令测试服务器:
telnet localhost 8080
Hello Server
服务器将响应:
服务器响应: HELLO SERVER
注意事项
此基础实现有几个关键问题需要注意:
- 无限制创建线程可能导致资源耗尽
- 缺乏线程管理机制
- 未处理客户端异常断开的情况
- 没有超时控制
学习目标
掌握ExecutorService线程池的使用,优化服务器资源管理
为什么使用线程池?
线程池优势
- 降低资源消耗:复用已创建的线程
- 提高响应速度:不需要等待线程创建
- 提高线程可管理性:统一分配和监控
- 防止服务器过载:控制最大并发数
线程池参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:任务队列
- threadFactory:线程创建工厂
- handler:拒绝策略
实现线程池服务器
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolServer {
private static final int PORT = 8080;
private static final int THREAD_POOL_SIZE = 10;
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("线程池服务器启动,端口:" + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("接受客户端连接:" +
clientSocket.getInetAddress().getHostAddress());
executor.execute(new ClientHandler(clientSocket));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
线程池选择建议
根据应用场景选择合适的线程池:
线程池类型 |
适用场景 |
特点 |
FixedThreadPool |
负载稳定的服务器 |
固定大小,队列无界 |
CachedThreadPool |
短时任务,负载变化大 |
可扩容,自动回收空闲线程 |
ScheduledThreadPool |
定时任务 |
支持定时及周期性执行 |
SingleThreadExecutor |
需要顺序执行的任务 |
单线程,保证执行顺序 |
学习目标
掌握完整的客户端请求处理流程,包括协议设计、异常处理和资源释放
增强版ClientHandler
import java.io.*;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class EnhancedClientHandler implements Runnable {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final Socket clientSocket;
private final String clientId;
public EnhancedClientHandler(Socket socket) {
this.clientSocket = socket;
this.clientId = socket.getInetAddress() + ":" + socket.getPort();
}
public void run() {
System.out.println("开始处理客户端: " + clientId);
try (
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
out.println("欢迎连接到多线程服务器! 输入'BYE'退出");
String inputLine;
while ((inputLine = in.readLine()) != null) {
String timestamp = LocalDateTime.now().format(FORMATTER);
System.out.printf("[%s] 客户端 %s: %s%n",
timestamp, clientId, inputLine);
if ("BYE".equalsIgnoreCase(inputLine)) {
out.println("再见! 连接关闭");
break;
}
String response = processRequest(inputLine);
out.println(response);
}
} catch (IOException e) {
System.err.println("处理客户端 " + clientId + " 请求时出错: " + e.getMessage());
} finally {
closeClient();
}
}
private String processRequest(String request) {
if (request.startsWith("TIME")) {
return "当前时间: " + LocalDateTime.now().format(FORMATTER);
} else if (request.startsWith("UPPER ")) {
return request.substring(6).toUpperCase();
} else if (request.startsWith("LOWER ")) {
return request.substring(6).toLowerCase();
} else {
return "未知命令,可用命令: TIME, UPPER, LOWER, BYE";
}
}
private void closeClient() {
try {
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
System.out.println("客户端连接已关闭: " + clientId);
}
} catch (IOException e) {
System.err.println("关闭客户端连接时出错: " + e.getMessage());
}
}
}
动手练习
- 添加命令历史记录功能
- 实现用户认证功能
- 添加请求频率限制
- 实现多协议支持(HTTP、自定义协议)
学习目标
识别并解决多线程服务器中的常见错误和性能问题
1. 线程安全问题
问题现象
多线程共享资源时出现数据不一致
示例:共享计数器不准确
解决方案
- 使用synchronized关键字保护临界区
- 使用ReentrantLock显式锁
- 使用AtomicInteger等原子类
- 使用ThreadLocal变量
2. 资源未关闭
问题现象
文件描述符耗尽,服务器无法接受新连接
错误:java.net.SocketException: Too many open files
解决方案
- 使用try-with-resources确保资源关闭
- 在finally块中显式关闭资源
- 使用Connection Pool管理数据库连接
- 添加关闭钩子(shutdown hook)
3. 线程池配置不当
问题现象
任务堆积导致内存溢出
响应延迟增加,服务器卡顿
解决方案
- 根据系统资源设置合理的线程池大小
- 使用有界队列防止内存溢出
- 实现合适的拒绝策略(AbortPolicy, CallerRunsPolicy等)
- 监控线程池状态
4. 死锁问题
问题现象
线程永久阻塞,服务器停止响应
CPU使用率低但请求无响应
解决方案
- 避免嵌套锁和多个锁的获取顺序不一致
- 使用tryLock()设置超时时间
- 使用jstack分析线程转储
- 使用并发工具类(如CountDownLatch)替代锁
5. 上下文切换过多
问题现象
CPU使用率高但吞吐量低
vmstat显示较高的cs(上下文切换)值
解决方案
- 减少线程数量,避免过多活动线程
- 使用异步I/O(NIO)减少线程需求
- 使用协程(Project Loom虚拟线程)
- 批处理任务减少切换
调试技巧
多线程服务器调试建议:
- 使用线程名称便于跟踪:Thread.currentThread().setName()
- 使用Thread Dump分析死锁:jstack PID
- 使用VisualVM或JConsole监控线程状态
- 添加详细的日志记录,包括线程ID
- 使用CountDownLatch模拟并发测试
I/O模型优化
- BIO:传统阻塞I/O,线程同步等待
- NIO:非阻塞I/O,使用选择器(Selector)
- AIO:异步I/O,回调机制
- 建议:高连接数场景使用NIO
内存优化
- 使用对象池减少GC压力
- 避免在循环中创建临时对象
- 使用直接缓冲区(Direct Buffer)
- 调整JVM堆大小和GC策略
网络优化
- 设置TCP_NODELAY减少延迟
- 调整SO_RCVBUF和SO_SNDBUF
- 启用SO_REUSEADDR
- 使用Keep-Alive连接
性能监控工具
工具 |
用途 |
命令/用法 |
JVisualVM |
监控JVM状态和线程 |
jvisualvm |
JConsole |
监控JVM指标 |
jconsole |
Jstack |
获取线程转储 |
jstack PID |
Jstat |
监控GC统计 |
jstat -gcutil PID 1000 |
Netstat |
查看网络连接 |
netstat -anp | grep PORT |
最佳实践
- 使用连接池管理数据库连接
- 异步处理耗时操作
- 分离I/O线程和业务线程
- 设置合理的超时时间
- 使用背压(backpressure)控制流量
- 定期进行压力测试
学习目标
巩固多线程服务器开发知识,了解进阶学习方向
知识总结
核心概念
- 线程生命周期与状态转换
- 线程同步与通信机制
- 线程池原理与配置
- 并发编程模型
- 锁与原子操作
实现技术
- ServerSocket与Socket API
- ExecutorService线程池
- I/O流操作
- 协议设计与实现
- 资源管理与异常处理
优化策略
- 性能监控与诊断
- JVM调优
- 网络参数优化
- 并发控制策略
- 容错与恢复机制
进阶方向
响应式编程
使用Reactor、RxJava等实现非阻塞、异步服务器
项目:Spring WebFlux
分布式系统
构建高可用、可扩展的分布式服务器集群
技术:Kubernetes、Service Mesh
虚拟线程
Java 19+ 虚拟线程(Project Loom)实现高并发
优势:轻量级、高吞吐
扩展项目
- 实现HTTP服务器支持GET/POST请求
- 添加SSL/TLS支持(HTTPS服务器)
- 实现基于WebSocket的实时通信
- 构建简单的RPC框架
- 添加监控和指标收集(Prometheus)
学习资源
- 书籍:《Java并发编程实战》
- 框架:Netty、Grizzly、Undertow
- 课程:Java NIO与Netty实战
- 工具:JMeter、Gatling压力测试
- 社区:Stack Overflow、GitHub