核心思想
一个简单的 Socket 服务器一次只能处理一个客户端的请求,当服务器正在处理客户端 A 的请求时,客户端 B 发来的连接请求只能排队等待,直到客户端 A 断开连接,这在高并发场景下是完全不可接受的。

多线程的解决方案是:
- 主线程(或称监听线程):只负责一个任务——在指定的端口上
accept()(接受)新的客户端连接。 - 工作线程:每当
accept()成功一个新连接,就立即创建一个新的线程来专门处理这个客户端的后续所有通信(读取数据、处理业务逻辑、发送响应)。
这样一来,主线程可以立即返回,继续监听下一个客户端连接,而每个客户端的通信任务都在自己的独立线程中并行处理,互不干扰。
编程模型
下面我们介绍两种最主流的模型,并给出代码示例。
为每个客户端创建一个线程(One-Thread-Per-Client)
这是最直观、最容易理解的模型。

工作流程:
- 服务器启动,创建一个
ServerSocket,绑定到指定端口。 - 在一个无限循环中,调用
serverSocket.accept()阻塞,等待客户端连接。 - 当一个客户端连接成功时,
accept()返回一个代表该客户端连接的Socket对象。 - 立即创建一个新的
ClientHandler线程,并将这个Socket对象传递给该线程。 - 主线程立即回到步骤 2,继续等待下一个客户端。
- 新创建的
ClientHandler线程负责与该客户端进行全生命周期的通信(读写数据),直到客户端断开连接后,线程结束。
优点:
- 逻辑简单:每个客户端一个线程,代码结构清晰,易于理解和实现。
- 并发性好:多个客户端可以真正地同时进行 I/O 操作。
缺点:
- 资源消耗大:每个客户端都会创建一个线程,如果客户端数量巨大(成千上万),JVM 中会存在大量线程,这会消耗大量内存和 CPU 进行线程上下文切换,导致服务器性能急剧下降甚至崩溃。
- 稳定性差:线程数量不受控制,容易因过多线程而导致系统资源耗尽。
代码示例:

服务器端代码 (MultiThreadServer.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadServer {
// 使用线程池来优化“为每个客户端创建一个线程”的模型
// 可以避免无限制地创建线程
private static final int THREAD_POOL_SIZE = 10;
public static void main(String[] args) {
int port = 8888;
// 创建一个固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器启动,监听端口 " + port + "...");
while (true) {
// accept() 是阻塞方法,等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接成功: " + clientSocket.getInetAddress().getHostAddress());
// 将处理客户端的任务提交给线程池执行
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
} finally {
// 关闭线程池
threadPool.shutdown();
}
}
}
// 客户端处理器,一个线程处理一个客户端
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 使用 try-with-resources 确保 I/O 流和 Socket 在使用后自动关闭
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
// readLine() 是阻塞方法,等待客户端发送数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
// 简单的回显服务器逻辑
out.println("服务器回复: " + inputLine);
}
} catch (IOException e) {
// 当客户端断开连接时,readLine() 会返回 null,此时会捕获到 SocketException
// 这是正常流程,可以打印日志或忽略
System.out.println("客户端 " + clientSocket.getInetAddress() + " 已断开连接。");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码 (SimpleClient.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class SimpleClient {
public static void main(String[] args) {
String hostname = "localhost";
int port = 8888;
try (
Socket socket = new Socket(hostname, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器,输入消息并发送,输入 'exit' 退出。");
String userInput;
while (true) {
System.out.print("请输入消息: ");
userInput = stdIn.readLine();
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
out.println(userInput);
String response = in.readLine();
System.out.println("服务器回复: " + response);
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
System.exit(1);
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
System.exit(1);
}
}
}
使用线程池(ThreadPool)
这是对模型一的重大优化,我们不再为每个客户端都创建一个新线程,而是从一个预先创建好的线程池中取出一个空闲线程来处理任务。
工作流程:
- 服务器启动,创建一个
ServerSocket和一个固定大小的ExecutorService(线程池)。 - 主线程进入无限循环,调用
serverSocket.accept()等待连接。 - 当一个客户端连接成功时,创建一个
Runnable任务(ClientHandler)。 - 将这个任务提交给线程池 (
threadPool.execute(task))。 - 主线程立即返回,继续监听。
- 线程池会从其内部的队列中取出一个空闲线程来执行这个任务,处理与该客户端的通信。
优点:
- 资源可控:线程数量上限由线程池大小决定,避免了因线程过多而导致系统崩溃。
- 性能提升:线程的创建和销毁是昂贵的操作,线程池复用已创建的线程,减少了这部分开销。
- 管理方便:可以方便地设置线程池的核心参数(核心大小、最大大小、队列等)。
缺点:
- 实现比模型一稍微复杂一点,但仍然是业界标准做法。
- 如果所有线程都在忙,新的客户端连接请求会在线程池的队列中等待,直到有线程空闲。
代码示例:
上面的 MultiThreadServer.java 已经是使用线程池的版本了!这是现代 Java 网络编程推荐的方式,我们再看一个更完整的例子,使用 ExecutorService 的 submit 方法。
// ... (ClientHandler 类与上面完全相同) ...
public class ThreadPoolServer {
private static final int PORT = 8888;
private static final int MAX_THREADS = 50; // 最大线程数
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("线程池服务器已启动,监听端口 " + PORT + "...");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("接受到新连接: " + clientSocket.getInetAddress());
// 提交任务到线程池
executor.submit(new ClientHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 优雅关闭服务器
System.out.println("正在关闭服务器...");
executor.shutdown(); // 停止接受新任务
// 可以添加等待当前任务完成的逻辑
// executor.awaitTermination(...);
}
}
}
高级模型:NIO (New I/O) 与非阻塞
当连接数变得非常巨大(C10K 问题)时,即使是线程池模型也可能力不从心,因为每个线程都需要消耗一定的内存和 CPU 资源。
Java NIO (New I/O) 提供了非阻塞 I/O 的解决方案,其核心思想是单线程处理多个连接。
核心组件:
- Channel (通道):类似传统的
Socket,但它是双向的,可以同时进行读写。 - Buffer (缓冲区):数据读写都通过
Buffer进行,数据先被读入Buffer,再从Buffer写出。 - Selector (选择器):这是 NIO 的精髓,一个
Selector可以轮询多个Channel的状态,判断它们是否已经准备好进行 I/O 操作(有数据可读,或可以写入数据)。
工作流程:
- 将所有客户端的
SocketChannel注册到Selector上,并指定我们关心的事件(如SelectionKey.OP_READ)。 - 启动一个或几个(通常很少)工作线程。
- 在一个循环中,调用
selector.select(),这个方法是阻塞的,但它不是在等待数据,而是在等待至少一个注册的Channel变为“就绪”状态。 - 当
select()返回后,获取所有“就绪”的Channel的集合。 - 遍历这个集合,对每个就绪的
Channel执行相应的 I/O 操作。 - 处理完后,继续下一次
select()循环。
优点:
- 极高的并发能力:用少量线程(甚至一个线程)就能处理成千上万的连接,资源消耗极低。
- 可伸缩性强:非常适合构建高并发的网络服务。
缺点:
- 编程模型复杂:需要理解
Channel,Buffer,Selector等概念,代码比传统 BIO 更难编写和调试。 - 粘包/半包问题:NIO 的 Buffer 模式需要自己处理 TCP 流中的数据边界问题,而 BIO 的
readLine()或固定长度读取则相对简单。
适用场景:
- 高性能 Web 服务器(如 Netty, Jetty 的底层就是 NIO)。
- 聊天服务器、游戏服务器等需要处理大量长连接的场景。
总结与选择
| 特性 | 每客户端一线程 | 线程池 | NIO 非阻塞 |
|---|---|---|---|
| 核心思想 | 一客户端一线程 | 线程池复用线程 | 单/多线程轮询多 Channel |
| 并发能力 | 低(受限于线程数) | 中(受限于线程池大小) | 极高(C10K+) |
| 资源消耗 | 高 | 中 | 低 |
| 实现复杂度 | 简单 | 较简单 | 复杂 |
| 适用场景 | 学习、演示、少量客户端 | 通用推荐、中等并发 | 高并发、高性能服务器 |
给你的建议:
- 初学者或学习目的:从“为每个客户端创建一个线程”的模型开始,理解其工作原理。
- 实际项目开发:直接使用线程池模型,它是性能、复杂度和稳定性的最佳平衡点,适用于绝大多数应用场景。
- 追求极致性能:学习并使用 NIO 模型,或者直接使用基于 NIO 的高性能网络框架,如 Netty、Mina 或 Vert.x,这些框架已经为你封装了 NIO 的复杂性,提供了更高级的 API。
