大家好,我是小黑,今天继续和大家分享网络编程中的阻塞式IO模型,这次的是多线程版本。
多线程版本的同步阻塞式IO模型
单线程版本的同步阻塞式IO模型,因为只有一个线程,无法充分利用现在机器多线程的能力,无法并行处理多个客户端请求。而多线程版本就没有这个问题,他的要点在于开多个线程,去处理客户端请求。
那能不能来一个客户端,就开一个线程处理呢?显然是不行的。原因在于:
- 线程的创建和销毁是要消耗系统资源的,线程太多会影响系统性能,
- 线程本身也会占用资源,开太多会有内存溢出的风险
所以实现多线程版本的同步阻塞式IO模型,更多是使用线程池的方式实现。利用线程复用的思想,减少创建和销毁线程的次数,并且根据具体的业务情况,灵活配置线程数
多线程版本同步阻塞式IO模型的实现
示例同样是一个Echo服务器,客户端发送什么数据,服务端就在加上一个日期然后原样返回。
我们先看看服务端怎么实现,代码如下。主体是一个死循环,阻塞在 accept() 方法上,有连接建立才会返回。返回后会包装一个 Handler 扔到线程池中,然后直接返回,等待下一个连接。
Handler 则负责处理网络数据,分为三个大步骤:读取客户端数据(read),处理客户端数据(process),结果返回给客户端(write)。
public class EchoServer04 {
private ExecutorService es;
private ServerSocket serverSocket;
public static void main(String[] args) throws IOException {
new EchoServer04().start();
}
public EchoServer04() throws IOException {
es = Executors.newCachedThreadPool(); // 一般都要根据业务场景设置好线程数和最大任务数
serverSocket = new ServerSocket(8001);
}
public void start() {
for (; ; ) { // 死循环,通过 accept() 方法不断接受客户端连接
Socket socket = null;
try {
socket = serverSocket.accept();
es.execute(new Handler(socket)); // 然后线程池,用异步线程的方式处理
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 网络数据处理任务,实现 Runnable 接口
private static class Handler implements Runnable {
public Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader bufferedReader = null;
PrintWriter printWriter = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
printWriter = new PrintWriter(socket.getOutputStream(), true);
long start = System.currentTimeMillis();
String line = null;
while ((line = bufferedReader.readLine()) != null) { // read
String echoMsg = new Date() + ": " + line; // process
printWriter.println(echoMsg); // write
}
long end = System.currentTimeMillis();
System.out.println("spend:" + (end - start));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (Objects.nonNull(printWriter)) {
printWriter.close();
}
if (Objects.nonNull(bufferedReader)) {
bufferedReader.close();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
复制代码
客户端一般单线程就可以了,如果需要代码在本地跑一跑,可以查看上一篇文章:同步阻塞式IO模型-单线程版本
多线程版本同步阻塞式IO模型的缺点
- 死锁风险
因为是多线程编程,所以就会有死锁的风险。日常工作中,我们一定要避免线程池中各个任务有依赖的关系。
- 系统资源
线程是很耗系统资源的,它本身要占用内存空间,线程的切换、创建、销毁也很耗资源。所以用多线程版本实现同步阻塞式IO的时候,一定要用线程池,而且要设置好最大线程数、最大任务数,避免请求量上来因为线程池但原因导致服务不可用。
- 同步阻塞式IO的原罪, read() 方法是阻塞的
我们可以试想一下,如果服务器要处理大量的客户端连接,而且网络环境不好,每个请求的 read() 方法都要阻塞很久,一旦达到线程池的上限,服务器就会无法再处理连接,但这个时候CPU并没有很忙,所以并发度达不到很高。
我们一起看看下面这个例子感受一下。这个客户端模拟了这样一种情况:并行发起10个网络连接,每个网络连接都只发送很少的数据,但是耗时都很长,这10个网络连接在服务端就占据了10个服务器线程,这些服务器线程只能等待数据完全读取完,才能去干别的事情,对线程资源是一种很大的浪费。
public class HeavySocketClient {
private static ExecutorService tp=Executors.newCachedThreadPool();
private static final int sleep_time=1000*1000*1000;
public static class EchoClient implements Runnable{
public void run(){
Socket client = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
client = new Socket();
client.connect(new InetSocketAddress("localhost", 8001));
writer = new PrintWriter(client.getOutputStream(), true);
writer.print("H");
LockSupport.parkNanos(sleep_time); // 等待1s,模拟网络环境不好的场景
writer.print("e");
LockSupport.parkNanos(sleep_time); // 等待1s,模拟网络环境不好的场景
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("l");
LockSupport.parkNanos(sleep_time);
writer.print("o");
LockSupport.parkNanos(sleep_time);
writer.print("!");
LockSupport.parkNanos(sleep_time);
writer.println(); // 发送数据
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
System.out.println("from server: " + reader.readLine()); // 接收数据并打印
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
if (reader != null)
reader.close();
if (client != null)
client.close();
} catch (IOException e) {
}
}
}
}
public static void main(String[] args) throws IOException {
EchoClient ec=new EchoClient();
for(int i=0;i<10;i++) { // 并行发起10个客户端连接,每个连接耗时都很长
tp.execute(ec);
}
}
}
复制代码
我们先看看客户端的结果。在数据发送完后,每一个客户端连接都收到了服务端的响应。
再看看服务端的结果,我们通过代码记录了一轮网络IO从读取到响应总共的耗时。每一个线程都因为客户端数据发的慢,都耗了6s才能才处理完一轮网络IO。
也正是因为这个问题,在大规模对性能有要求的网络应用场景中,多线程版本的同步阻塞式IO模型很少被采用。
那么,大规模对性能有要求的网络应用场景中都采用哪种IO模型呢?那就是多路复用IO了,稍等,马上更新,欢迎关注专栏:网络编程精粹
白白 ~
Ref
- 极客时间:《深入拆解Tomcat&Jetty》
- 孙卫琴:《Java网络编程精解》
- Budi Kurniawan、Paul Deck:《深入剖析Tomcat》
- 葛一鸣:《实战Java高并发程序设计》