高级Java工程师必备 ----- 深入分析 Java IO (一)BIO

BIO编程

最原始BIO

网络编程的基本模型是C/S模型,即两个进程间的通信。

服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
最原始BIO通信模型图:

存在的问题:

  • 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。(acceptor只有在接受完client1的请求后才能接受client2的请求)
  • 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。

一请求一线程BIO

那有没有方法改进呢? ,答案是有的。改进后BIO通信模型图:

此种BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

代码演示

服务端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServer {
   //默认的端口号
   private static int DEFAULT_PORT = 8083;  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("监听来自于"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               SocketServerThread socketServerThread = new SocketServerThread(socket);
               new Thread(socketServerThread).start();
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServer.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThread implements Runnable {
   private Socket socket;
   public SocketServerThread (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我们收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用线程,同样无法解决read方法的阻塞问题,
           //也就是说read方法处同样会被阻塞,直到操作系统有数据准备好
           int realLen = in.read(contextBytes, 0, maxLen);
           //读取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服务器收到来自于端口:" + sourcePort + "的信息:" + message);

           //下面开始发送信息
           out.write("回发响应信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //试图关闭
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

客户端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch;

public class BioSocketClient{
   public static void main(String[] args) throws Exception {
       Integer clientNumber = 20;
       CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

       // 分别开始启动这20个客户端,并发访问
       for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
           ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
           new Thread(client).start();
       }

       // 这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
       synchronized (BioSocketClient.class) {
           BioSocketClient.class.wait();
       }
   }
}

/**
* 一个ClientRequestThread线程模拟一个客户端请求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable {

   private CountDownLatch countDownLatch;

   /**
    * 这个线程的编号
    * @param countDownLatch
    */
   private Integer clientIndex;

   /**
    * countDownLatch是java提供的同步计数器。
    * 当计数器数值减为0时,所有受其影响而等待的线程将会被激活。这样保证模拟并发请求的真实性
    * @param countDownLatch
    */
   public ClientRequestThread(CountDownLatch countDownLatch , Integer clientIndex) {
       this.countDownLatch = countDownLatch;
       this.clientIndex = clientIndex;
   }

   @Override
   public void run() {
       Socket socket = null;
       OutputStream clientRequest = null;
       InputStream clientResponse = null;

       try {
           socket = new Socket("localhost",8083);
           clientRequest = socket.getOutputStream();
           clientResponse = socket.getInputStream();

           //等待,直到SocketClientDaemon完成所有线程的启动,然后所有线程一起发送请求
           this.countDownLatch.await();

           //发送请求信息
           clientRequest.write(("这是第" + this.clientIndex + " 个客户端的请求。 over").getBytes());
           clientRequest.flush();

           //在这里等待,直到服务器返回信息
          System.out.println("第" + this.clientIndex + "个客户端的请求发送完成,等待服务器返回信息");
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           int realLen;
           String message = "";
           //程序执行到这里,会一直等待服务器返回信息(注意,前提是in和out都不能close,如果close了就收不到服务器的反馈了)
           while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
               message += new String(contextBytes , 0 , realLen);
           }
           //String messageEncode = new String(message , "UTF-8");
           message = URLDecoder.decode(message, "UTF-8");
           System.out.println("第" + this.clientIndex + "个客户端接收到来自服务器的信息:" + message);
       } catch (Exception e) {

       } finally {
           try {
               if(clientRequest != null) {
                   clientRequest.close();
               }
               if(clientResponse != null) {
                   clientResponse.close();
               }
           } catch (IOException e) {

           }
       }
   }
}   

存在的问题:

  • 虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知accept()的方式还是单个的。也就是,实际上是服务器接收到数据报文后的“业务处理过程”可以多线程,但是数据报文的接受还是需要一个一个的来(acceptor只有在接受完client1的请求后才能接受client2的请求),下文会验证。
  • 在linux系统中,可以创建的线程是有限的。我们可以通过cat /proc/sys/kernel/threads-max命令查看可以创建的最大线程数。当然这个值是可以更改的,但是线程越多,CPU切换所需的时间也就越长,用来处理真正业务的需求也就越少。
  • 另外,如果您的应用程序大量使用长连接的话,线程是不会关闭的。这样系统资源的消耗更容易失控。

伪异步I/O编程

为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。

伪异步I/O模型图:

代码演示

只给出服务端,客户端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServerThreadPool {
   //默认的端口号
   private static int DEFAULT_PORT = 8083;
   //线程池 懒汉式的单例
   private static ExecutorService executorService = Executors.newFixedThreadPool(60);  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("监听来自于"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               //当然业务处理过程可以交给一个线程(这里可以使用线程池),并且线程的创建是很耗资源的。
               //最终改变不了.accept()只能一个一个接受socket的情况,并且被阻塞的情况
               SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
               executorService.execute(socketServerThreadPool);
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServerThreadPool.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThreadPool implements Runnable {
   private Socket socket;
   public SocketServerThreadPool (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我们收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用线程,同样无法解决read方法的阻塞问题,
           //也就是说read方法处同样会被阻塞,直到操作系统有数据准备好
           int realLen = in.read(contextBytes, 0, maxLen);
           //读取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服务器收到来自于端口:" + sourcePort + "的信息:" + message);

           //下面开始发送信息
           out.write("回发响应信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //试图关闭
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

服务器端的执行效果

在 Socket socket = serverSocket.accept(); 处打了断点,有20个客户端同时发出请求,可服务端还是一个一个的处理,其它线程都处于阻塞状态

阻塞的问题根源

那么重点的问题并不是“是否使用了多线程、或是线程池”,而是为什么accept()、read()方法会被阻塞。API文档中对于 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服务器线程发起一个accept动作,询问操作系统 是否有新的socket套接字信息从端口xx发送过来。

注意,是询问操作系统。也就是说socket套接字的IO模式支持是基于操作系统的,那么自然同步IO/异步IO的支持就是需要操作系统级别的了。如下图:

如果操作系统没有发现有套接字从指定的端口xx来,那么操作系统就会等待。这样serverSocket.accept()方法就会一直等待。这就是为什么accept()方法为什么会阻塞:它内部的实现是使用的操作系统级别的同步IO。

  • 阻塞IO 和 非阻塞IO
    这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
  • 同步IO 和非同步IO
    这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何处理相应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

原文地址:https://www.cnblogs.com/java-chen-hao/p/11076176.html

时间: 2024-10-16 08:05:20

高级Java工程师必备 ----- 深入分析 Java IO (一)BIO的相关文章

高级Java工程师必备 ----- 深入分析 Java IO (三)

概述 Java IO即Java 输入输出系统.不管我们编写何种应用,都难免和各种输入输出相关的媒介打交道,其实和媒介进行IO的过程是十分复杂的,这要考虑的因素特别多,比如我们要考虑和哪种媒介进行IO(文件.控制台.网络),我们还要考虑具体和它们的通信方式(顺序.随机.二进制.按字符.按字.按行等等).Java类库的设计者通过设计大量的类来攻克这些难题,这些类就位于java.io包中. 在JDK1.4之后,为了提高Java IO的效率,Java又提供了一套新的IO,Java New IO简称Jav

Java工程师必备

JAVA基础扎实,熟悉JVM,熟悉网络.多线程.分布式编程及性能调优 精通Java EE相关技术 熟练运用Spring/SpringBoot/MyBatis等基础框架 熟悉分布式系统相关技术 熟悉MySQL及SQL优化 高并发.高可用.微服务.容器化等开发经验 熟悉JVM原理,精通io.多线程,精通分布式.缓存.消息等机制 熟练使用和配置Tomcat应用服务器 掌握Eclispse,Maven,SVN,GIT等软件开发工具的环境搭建和使用 熟悉H5.CSS.JS等前端技术,熟悉BootStrap

《Java Web开发实战》——Java工程师必备干货教材

一年一度毕业季,又到了简历.offer漫天飞,失望与希望并存的时节.在IT行业,高校毕业生求职时,面临的第一道门槛就是技能与经验的考验,但学校往往更注重学生的理论知识,忽略了对学生实践能力的培养,因而导致很多求职者在面试中败下阵来.在毕业生所学知识普遍脱离职场实战的大趋势下,<Java Web开发实战>应运而生,此教材由千锋教研院高教产品研发部编著.清华大学出版社出版,将理论应用于实践,切实提高学生的实战能力. 整合多年实战案例 助力Java人才快速成长 <Java Web开发实战>

从零开始搭建Java开发环境第一篇:Java工程师必备软件大合集

1.JDK https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 目前主流的JDK版本还是JAVA8,我在阿里用的也是Java8. JDK里已经包含了JRE也就是Java虚拟机和运行环境,无需另外下载安装. 2.MySQL 和 Navicat for MySQL 数据库一般还是用免费的MySQL,这里推荐8.0以上的版本,具体怎么安装可以看这篇文章:https://mp.csdn.

高级.net工程师必备

<div><p style="margin:0.7em">Microsoft SQL Server 数据库<br/>一.创建和维护数据库<br/>1.数据库<br/> SQL Server 数据库的组成部分?(参见联机丛书)<br/> 如何保证数据库的完整性.安全性.并发性?<br/> 数据库设计创建步骤?(参见联机丛书 - 创建数据库计划)<br/> 数据库设计考虑的事项?(参见联机丛书)

React 源码深度解析 高级前端工程师必备技能完整教程百度云

原文配套资源获取链接:点击获取 第1章 课程导学 对课程整体进行讲解. 1-1 课程导学 第2章 基础知识 React API 一览 React主要API介绍,在这里你能了解它的用法,为下一章源码分析打基础. 2-1 准备工作 2-2 JSX到JavaScript的转换 2-3 react-element 2-4 react-component 2-5 react-ref 2-6 forward-ref 2-7 context 2-8 concurrent-mode 2-9 suspense-a

React源码深度解析 高级前端工程师必备技能

String 课程地址 = " http://icourse8.com/reactyuanma.html "; 章节信息第1章 课程导学 第2章 基础知识 React API 一览 第3章 React中的更新 第4章 Fiber Scheduler 第5章 各类组件的Update 第6章 完成节点任务 第7章 commitRoot 第8章 功能详解:基础 第9章 suspense and priority 第10章 功能详解:Hooks 第11章 课程总结 class Solution

巨人大哥谈Java工程师高手之路

巨人大哥谈Java工程师高手之路 JVM方面 JVM内存结构 堆.栈.方法区.直接内存.堆和栈区别 Java内存模型 内存可见性.重排序.顺序一致性.volatile.锁.final 垃圾回收 内存分配策略.垃圾收集器(G1).GC算法.GC参数.对象存活的判定 JVM参数及调优 Java对象模型 oop-klass.对象头 HotSpot 即时编译器.编译优化 类加载机制 classLoader.类加载过程.双亲委派(破坏双亲委派).模块化(jboss modules.osgi.jigsaw)

转:Java工程师成神之路~(2018修订版)

转: http://www.hollischuang.com/archives/489 阿里大牛珍藏架构资料,点击链接免费获取 针对本文,博主最近在写<成神之路系列文章> ,分章分节介绍所有知识点.欢迎关注. 主要版本 更新时间 备注 v1.0 2015-08-01 首次发布 v1.1 2018-03-12 增加新技术知识.完善知识体系 一.基础篇 JVM JVM内存结构 堆.栈.方法区.直接内存.堆和栈区别 Java内存模型 内存可见性.重排序.顺序一致性.volatile.锁.final