第一篇 《 Java 中的 IO 》
一 简单的介绍
1. IO 是什么?
是英文 Input 输入、Output 输出的英文缩写统一为IO。
2. java 的 IO 是什么?
首先操作系统为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为 两部分,一部分是内核空间(Kernel-Space),一部分是用户空间(User-Space)。
内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在内核空间区域进行读写,也是不容许直接调用内核代码定义的函数的,要进行系统调用的时候,就要将进程切换到内核态才能进行。
用户程序进行IO的读写,依赖于底层的IO读写,上层应用通过操作系统的sys_read、sys_write系统调用,是把数据从内核缓冲区(read)或应用程序的进程缓冲区(write) 复制到应用程序的进程缓冲区(read)或操作系统内核缓冲区(write)。
应用程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。无论是对Socket的IO操作,还是对文件的IO操作,都属于上层应用开发,都是在内核缓冲区和进程缓冲区之间的进行数据交换。
那么java 应用程序 就是上层应用开发。
3. 五种主要的IO模型
-
- 同步阻塞IO(BlockingIO)指的是用户空间(或者线程)主动发起,需要等待内核IO操作彻底 完成后,才返回到用户空间的IO操作,IO操作过程中,发起IO请求的用户进程(或者线程) 处于阻塞状态。
- 同步非阻塞NIO(Non-Blocking IO)指的是用户进程主动发起,不需要等待内核IO操作彻底完成之后, 就能立即返回到用户空间的IO操作,IO操作过程中,发起IO请求的用户进程(或者线程) 处于非阻塞状态。
- IO多路复用(IOMultiplexing)高性能Reactor线程模型的基础IO模型,当然,此模型 是建立在同步非阻塞的模型基础之上的升级版。
- 信号驱动IO模型 () 可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。只是 信号驱动IO仅仅在IO事件的通知阶段是异 步的,而在第二阶段,也就是在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程 是阻塞的、同步的。
- 异步IO(Asynchronous IO)异步非阻塞 指的是用户空间与内核空间的调用方式大反转。用户空间的线程变成被动接 受者,而内核空间成了主动调用者。当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。
二 Java 的 BIO (同步阻塞IO)
就是传统的 Java IO 编程,其相关的类和接口在 java.io包中。
BIO 同步阻塞IO,服务器实现为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
产生的问题:
1.基本使用的是流的方式获取数据, InputStream、OutputStream 单向流。
2.结合线程池使用,线程池分配专门的线程去处理。每个线程都独自处理自己负责的输入和输出,严重依赖于线程。
-
-
- 线程的创建和销毁成本很高
- 线程本身占用较大内存
- 线程的切换成本很高
- 容易造成锯齿状的系统负载。
-
示例代码:
public class BIOServer { public static void main(String[] args) throws IOException { //创建一个线程池 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); ServerSocket serverSocket = new ServerSocket(7777); System.out.println("服务器启动了 7777"); while (true) { //如果有连接 就创建一个线程 通讯 System.out.println("等待连接。。。");//没有连接时阻塞在这里 final Socket socket = serverSocket.accept(); System.out.println("有一个连接"); newCachedThreadPool.execute(new Runnable() { public void run() { handler(socket); } }); } } //编写 一个 handler 方法 和客户端通讯 public static void handler(Socket socket) { System.out.println("线程id=="+Thread.currentThread().getId()+"线程名字"+Thread.currentThread().getName()); byte[] bytes = new byte[1024]; try { //通过socket 获得输入流 InputStream inputStream = socket.getInputStream(); //循环读取 数据 while (true) { System.out.println("线程id=="+Thread.currentThread().getId()+"线程名字"+Thread.currentThread().getName()); System.out.println("等待read。。。。");//没有数据时阻塞在这里 int len = inputStream.read(bytes); if (len != -1) { //输出 发送来的数据 System.out.println(new String(bytes, 0, len)); } else { break; } } } catch (IOException e) { throw new RuntimeException(e); } finally { try { socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } }
使用telnet 连接尝试发送数据:
输出结果:
服务器启动了 7777 等待连接。。。 有一个连接 等待连接。。。 线程id==24线程名字pool-1-thread-1 线程id==24线程名字pool-1-thread-1 等待read。。。。 1 线程id==24线程名字pool-1-thread-1 等待read。。。。 有一个连接 等待连接。。。 线程id==25线程名字pool-1-thread-2 线程id==25线程名字pool-1-thread-2 等待read。。。。 hai 线程id==25线程名字pool-1-thread-2 等待read。。。。
问题分析:
1)每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据 Write
2)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
三 Java NIO( New IO )
在1.4版本之前,Java IO类库是阻塞式IO;从1.4版本开始,引进了新的异步IO库,被称 为Java New IO类库,简称为Java NIO。Java NIO 类库的目标,就是要让Java支持非阻塞IO(Non-BlockIO)。
NIO弥补了原来面向流的BIO(同步阻塞IO)的不足,提供了高速的、面向缓冲区的IO。(NIO是 面向缓冲区,或者面向块编程的。数据读取到一个 稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络)
Java NIO的非阻塞模式:
一个线程从某通道发送请求或者读取数据时,它仅能得到目前可用的数据,如果没有数据可用时,就什么都不会获取,而不是保持线程阻塞,直至数据变的可读取之前,该线程可以继续做其他的事情。写也是如此,一个线程请求写入一些数据到某通道时,不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解: NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来根据实际情况,可以分配50或者100个线程来处理,不像之前的阻塞IO那样,非得分配10000个。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
Java NIO 类库包含以下三个核心组件: Selector(选择器)、 Channel(通道)、 Buffer(缓冲区)。
BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
三者的关系:
Selector 对应一个线程,一个线程对应多个 channel ,下图反应了有四个channel注册到该selector。Selector 会根据不同的事件,在各个 channel 通道上切换,切换到哪个channel是由事件决定的。
channel是双向的 ,可以返回底层操作系统的情况,比如Linux,底层的操作系统 通道就是双向的。
每个channel 都会对应一个Buffer , Buffer 就是一个内存块,底层是一个数组, 数据的读取写入通过 Buffer , NIO的Buffer 是可以读也可以写,需要 fip 方法切换。
下一篇 对 Java NIO 三大核心 selector channel buffer 知识点梳理