微网站开发报价单,wordpress标题怎么,管理咨询的作用,工程建设信息网站I/O 一直是很多Java同学难以理解的一个知识点#xff0c;这篇帖子将会从底层原理上带你理解I/O#xff0c;让你看清I/O相关问题的本质。
1、I/O的概念
I/O 的全称是Input/Output。虽常谈及I/O#xff0c;但想必你也一时不能给出一个完整的定义。搜索了谷哥欠#xff0c;发…I/O 一直是很多Java同学难以理解的一个知识点这篇帖子将会从底层原理上带你理解I/O让你看清I/O相关问题的本质。
1、I/O的概念
I/O 的全称是Input/Output。虽常谈及I/O但想必你也一时不能给出一个完整的定义。搜索了谷哥欠发现也尽是些冗长的论述。要想厘清I/O这个概念我们需要从不同的视角去理解它。
1.1、计算机结构的视角
根据冯.诺依曼结构计算机结构分为 5 大部分运算器、控制器、存储器、输入设备、输出设备。其中输入是指将数据输入到计算机的设备比如键盘鼠标输出是指从计算机中获取数据的设备比如显示器以及既是输入又是输出设备硬盘网卡等。 用户通过操作系统才能完成对计算机的操作。计算机启动时第一个启动的程序是操作系统的内核它将负责计算机的资源管理和进程的调度。换句话说操作系统负责从输入设备读取数据并将数据写入到输出设备。
1.2、程序应用的视角
根据大学里学到的操作系统相关的知识为了保证操作系统的稳定性和安全性一个进程的地址空间划分为 用户空间User space 和 内核空间Kernel space 。
应用程序作为一个文件保存在磁盘中只有加载到内存到成为一个进程才能运行。应用程序运行在计算机内存中必然会涉及到数据交换比如读写磁盘文件访问数据库调用远程API等等。但我们编写的程序并不能像操作系统内核一样直接进行I/O操作。
从应用程序的视角来看的话我们的应用程序对操作系统的内核发起 IO 调用系统调用操作系统负责的内核执行具体的 IO 操作。也就是说我们的应用程序实际上只是发起了 IO 操作的调用而已具体 IO 的执行是由操作系统的内核来完成的。
但操作系统向外提供API其由各种类型的系统调用System Call组成以提供安全的访问控制。所以应用程序要想访问内核管理的I/O必须通过调用内核提供的系统调用(system call进行间接访问。
所以I/O之于应用程序来说强调的通过向内核发起系统调用完成对I/O的间接访问。换句话说应用程序发起的一次IO操作实际包含两个阶段
IO调用阶段应用程序进程向内核发起系统调用。IO执行阶段内核执行IO操作并返回。准备数据阶段内核等待I/O设备准备好数据拷贝数据阶段将数据从内核缓冲区拷贝到用户空间缓冲区。
UNIX 系统下 IO 模型一共有 5 种
同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O异步 I/O。
推荐孙卫琴老师的书籍
2、BIO (Blocking I/O)
2.1、BIO模型解析
BIO即同步阻塞IO实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时服务器就会开启一个线程去处理这个请求即使这个请求不干任何事情这个线程都一直处于阻塞状态。
应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前若发起系统调用的线程一直处于等待状态则此次IO操作为阻塞IO。阻塞IO简称BIOBlocking IO。其处理流程如下图所示 从上图可知当用户进程发起IO系统调用后内核从准备数据到拷贝数据到用户空间的两个阶段期间用户调用线程选择阻塞等待数据返回。
因此BIO带来了一个问题如果内核数据需要耗时很久才能准备好那么用户进程将被阻塞浪费性能。为了提升应用的性能虽然可以通过多线程来提升性能但线程的创建依然会借助系统调用同时多线程会导致频繁的线程上下文的切换同样会影响性能。所以要想解决BIO带来的问题我们就得看到问题的本质那就是阻塞二字。
BIO模型有很多缺点最大的缺点就是资源的浪费。想象一下如果QQ使用BIO模型当有一个人上线时就需要一个线程即使这个人不聊天这个线程也一直被占用那再多的服务器资源都不管用。
2.2、BIO代码演示
使用 BIO 模型编写一个服务器端监听 6666 端口当有客户端连接时就启动一个线程与之通讯。
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** title BIOServer* description 测试* author: yangyongbing* date: 2023/12/7 11:45*/
public class BIOServer {public static void main(String[] args) throws IOException {ExecutorService newCachedThreadPool Executors.newCachedThreadPool();ServerSocket serverSocket new ServerSocket(8888);System.out.println(服务器启动了);while (true){System.out.println(线程信息 id Thread.currentThread().getId() 名字 Thread.currentThread().getName());//监听等待客户端连接System.out.println(等待连接....);final Socket socket serverSocket.accept();System.out.println(连接到一个客户端);//一个客户端连接就创建一个线程并与之建立通讯newCachedThreadPool.execute(new Runnable() {Overridepublic void run() {//与客户端建立通讯handler(socket);}});}}public static void handler(Socket socket) {try {System.out.println(线程信息 id Thread.currentThread().getId() 名字 Thread.currentThread().getName());byte[] bytes new byte[1024];// 通过socket 获取输入流InputStream inputStream socket.getInputStream();// 循环的读取客户端发送的数据while (true) {System.out.println(线程信息 id Thread.currentThread().getId() 名字 Thread.currentThread().getName());System.out.println(read....);int index inputStream.read(bytes);if (index ! -1) {// 输出客户端发送的数据System.out.println(new String(bytes, 0, index));} else {break;}}} catch (Exception e) {e.printStackTrace();} finally {System.out.println(关闭和客户端的连接);try {socket.close();} catch (IOException e) {e.printStackTrace();}}}
}
3、NIO (Non-blocking/New I/O)
3.1、NIO模型解析
Java NIO 全称 Java non-blocking IO是指 JDK 提供的新 API。从 JDK1.4 开始Java 提供了一系列改进的输入/输出的新特性被统称为 NIO即 NewIO是同步非阻塞的。
就是用户进程在发起系统调用时指定为非阻塞内核接收到请求后就会立即返回然后用户进程通过轮询的方式来拉取处理结果。也就是如下图所示 应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前若发起系统调用的线程不会等待而是立即返回则此次IO操作为非阻塞IO模型。非阻塞IO简称NIONon-Blocking IO。
BIO是阻塞的如果没有多线程BIO就需要一直占用CPU而NIO则是非阻塞IONIO在获取连接或者请求时即使没有取得连接和数据也不会阻塞程序。NIO的服务器实现模式为一个线程可以处理多个请求连接。 NIO有几个知识点需要掌握Channel(通道)Buffer(缓冲区), Selector多路复用选择器
Channel既可以用来进行读操作又可以用来进行写操作。NIO中常用的Channel有FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。Buffer缓冲区用来发送和接受数据。Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个用于检查一个或多个NIO Channel通道的状态是否处于可读、可写。Java在NIO中使用Selector往往是将Channel注册到Selector中如下图所示 然而非阻塞IO虽然相对于阻塞IO大幅提升了性能但依旧不是完美的解决方案其依然存在性能问题也就是频繁的轮询导致频繁的系统调用会耗费大量的CPU资源。比如当并发很高时假设有1000个并发那么单位时间循环内将会有1000次系统调用去轮询执行结果而实际上可能只有2个请求结果执行完毕这就会有998次无效的系统调用造成严重的性能浪费。有问题就要解决那NIO问题的本质就是频繁轮询导致的无效系统调用。
2.2、NIO代码演示
NIO服务端的执行过程是这样的
创建一个ServerSocketChannel和Selector然后将ServerSocketChannel注册到Selector上Selector通过select方法去轮询监听channel事件如果有客户端要连接时监听到连接事件通过channel方法将socketchannel绑定到ServerSocketChannel上绑定通过SelectorKey实现socketchannel注册到Selector上关联读事件Selector通过select方法去轮询监听channel事件当监听到有读事件时ServerSocketChannel通过绑定的SelectorKey定位到具体的channel读取里面的数据。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;/*** title NIOServer* description NIO测试* author: yangyongbing* date: 2023/12/7 12:02*/
public class NIOServer {public static void main(String[] args) throws IOException{//创建一个socket通道并且设置为非阻塞的方式ServerSocketChannel serverSocketChannelServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(9000));//创建一个selector选择器把channel注册到selector选择器上Selector selectorSelector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true){System.out.println(等待事件发生);selector.select();System.out.println(有事件发生了);IteratorSelectionKey iterator selector.selectedKeys().iterator();while (iterator.hasNext()){SelectionKey key iterator.next();iterator.remove();handler(key);}}}private static void handler(SelectionKey key) throws IOException {if (key.isAcceptable()){System.out.println(连接事件发生);ServerSocketChannel serverSocketChannel (ServerSocketChannel) key.channel();//创建客户端一侧的channel并注册到selector上SocketChannel socketChannel serverSocketChannel.accept();socketChannel.configureBlocking(false);socketChannel.register(key.selector(),SelectionKey.OP_READ);}else if (key.isReadable()){System.out.println(数据可读事件发生);SocketChannel socketChannel (SocketChannel) key.channel();ByteBuffer buffer ByteBuffer.allocate(1024);int len socketChannel.read(buffer);if (len!-1){System.out.println(读取到客户端发送的数据new String(buffer.array(),0,len));}//给客户端发送信息ByteBuffer wrap ByteBuffer.wrap(hello world.getBytes());socketChannel.write(wrap);key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);socketChannel.close();}}}
客户端代码NIO客户端代码的实现比BIO复杂很多主要的区别在于NIO的客户端也需要去轮询自己和服务端的连接情况
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;/*** title NIOClient* description NIOClient测试* author: yangyongbing* date: 2023/12/7 12:19*/
public class NIOClient {public static void main(String[] args) throws IOException {//配置基本的连接参数SocketChannel channel SocketChannel.open();channel.configureBlocking(false);Selector selector Selector.open();channel.connect(new InetSocketAddress(127.0.0.1, 9000));channel.register(selector, SelectionKey.OP_CONNECT);//轮询访问selectorwhile (true) {selector.select();IteratorSelectionKey iterator selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();iterator.remove();//连接事件发生if (key.isConnectable()) {SocketChannel socketChannel (SocketChannel) key.channel();//如果正在连接则完成连接if (socketChannel.isConnectionPending()) {socketChannel.finishConnect();}socketChannel.configureBlocking(false);ByteBuffer buffer ByteBuffer.wrap(客户端发送的数据.getBytes());socketChannel.write(buffer);socketChannel.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {//读取服务端发送过来的消息read(key);}}}}private static void read(SelectionKey key) throws IOException {SocketChannel socketChannel (SocketChannel) key.channel();ByteBuffer buffer ByteBuffer.allocate(512);int len socketChannel.read(buffer);if (len ! -1) {System.out.println(客户端收到信息 new String(buffer.array(), 0, len));}}}效果大概是这样的首先服务端等待事件发生当客户端启动时服务器端先接受到连接的请求接着接受到数据读取的请求读完数据后继续等待。
NIO通过一个Selector负责监听各种IO事件的发生然后交给后端的线程去处理。NIO相比与BIO而言非阻塞体现在轮询处理上。BIO后端线程需要阻塞等待客户端写数据如果客户端不写数据就一直处于阻塞状态。而NIO通过Selector进行轮询已注册的客户端当有事件发生时才会交给后端去处理后端线程不需要等待。
3、AIO (Non-blocking/New I/O)
3.1、AIO模型解析
异步 IO 是基于事件和回调机制实现的也就是应用操作之后会直接返回不会堵塞在那里当后台处理完成操作系统会通知相应的线程进行后续的操作。 目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO不过又放弃了。这是因为Netty 使用了 AIO 之后在 Linux 系统上的性能并没有多少提升。
3.2、AIO代码演示
AIO是在JDK1.7中推出的新的IO方式–异步非阻塞IO也被称为NIO2.0AIO在进行读写操作时直接调用API的read和write方法即可这两种均是异步的方法且完成后会主动调用回调函数。简单来讲当有流可读取时操作系统会将可读的流传入read方法的缓冲区并通知应用程序对于写操作而言当操作系统将write方法传递的流写入完毕时操作系统主动通知应用程序。
Java提供了四个异步通道AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
服务器端代码AIO的创建方式和NIO类似先创建通道再绑定再监听。只不过AIO中使用了异步的通道。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.concurrent.TimeUnit;/*** title AIOServer* description AIOServer测试* author: yangyongbing* date: 2023/12/7 12:41*/
public class AIOServer {public static void main(String[] args) {try {//创建异步通道AsynchronousServerSocketChannel serverSocketChannelAsynchronousServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8080));System.out.println(等待连接中);//在AIO中accept有两个参数// 第一个参数是一个泛型可以用来控制想传递的对象// 第二个参数CompletionHandler用来处理监听成功和失败的逻辑// 如此设置监听的原因是因为这里的监听是一个类似于递归的操作每次监听成功后要开启下一个监听serverSocketChannel.accept(null, new CompletionHandlerAsynchronousSocketChannel, Object() {//请求成功处理逻辑Overridepublic void completed(AsynchronousSocketChannel result, Object attachment) {System.out.println(连接成功处理数据中);//开启新的监听serverSocketChannel.accept(null,this);handlerData(result);}Overridepublic void failed(Throwable exc, Object attachment) {System.out.println(失败);}});try {TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}} catch (IOException e) {e.printStackTrace();}}private static void handlerData(AsynchronousSocketChannel result) {ByteBuffer byteBufferByteBuffer.allocate(1024);//通道的read方法也带有三个参数//1.目的地处理客户端传递数据的中转缓存可以不使用//2.处理客户端传递数据的对象//3.处理逻辑也有成功和不成功的两个写法result.read(byteBuffer, byteBuffer, new CompletionHandlerInteger, ByteBuffer() {Overridepublic void completed(Integer result, ByteBuffer attachment) {if (result0){attachment.flip();byte[] array attachment.array();System.out.println(new String(array));}}Overridepublic void failed(Throwable exc, ByteBuffer attachment) {System.out.println(失败);}});}
}客户端代码:主要实现数据的发送功能
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Scanner;/*** title AIOClient* description AIOClient* author: yangyongbing* date: 2023/12/7 12:44*/
public class AIOClient {public static void main(String[] args) {try {AsynchronousSocketChannel socketChannel AsynchronousSocketChannel.open();socketChannel.connect(new InetSocketAddress(127.0.0.1, 8080));Scanner scanner new Scanner(System.in);String next scanner.next();ByteBuffer byteBuffer ByteBuffer.allocate(1024);byteBuffer.put(next.getBytes());byteBuffer.flip();socketChannel.write(byteBuffer);} catch (IOException e) {e.printStackTrace();}}
}
4、总结
I/O 其关键点是要将应用程序的IO操作分为两个步骤来理解IO调用和IO执行。IO调用才是应用程序干的事情而IO执行是操作系统的工作。在IO调用时对待操作系统IO就绪状态的不同方式决定了其是阻塞或非阻塞模式在IO执行时线程或进程是否挂起等待IO执行决定了其是否为同步或异步IO。