当前位置: 首页 > news >正文

Java 输入与输出之 NIO【非阻塞式IO】【NIO核心原理】探索之【一】

Java标准的输入/输出(Input/Output,简称I/O)是Java程序与外部世界进行交互的重要机制,它允许程序读取和写入数据到各种类型的源,如文件、网络套接字、管道、内存缓冲区等。Java I/O API主要位于java.io包中,提供了丰富的类和接口来处理不同类型的输入输出操作。
Java 的 I/O 类库位于 java.io 包中,JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
Java标准的输入/输出(I/O)体系一些实现类的层次关系:
在这里插入图片描述

以下是一个简单的 Java I/O 示例,它展示了如何使用 FileInputStream 和 FileOutputStream 读取和写入文件:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class IOExample {public static void main(String[] args) {try {// 创建输入流以读取文件FileInputStream fis = new FileInputStream("input.txt");// 创建输出流以写入文件FileOutputStream fos = new FileOutputStream("output.txt");int content;// 读取并写入文件直到文件末尾while ((content = fis.read()) != -1) {fos.write(content);}// 关闭流fis.close();fos.close();} catch (IOException e) {e.printStackTrace();}}
}

一、Java I/O发展史
JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
JDK 1.4 引入了一套全新的IO处理机制,引入了缓存区(Buffer)、通道(Channel)等概念,与之前的标准IO(BIO)相比,NIO具有更高的可扩展性和灵活性,特别是在网络编程和高并发场景下,表现得更为出色。这是非阻塞式IO(NIO)模式。提供了更强大的文件处理功能和更高效的IO操作,如内存映射文件等的功能。Java NIO(New I/O)是一种高性能的I/O处理机制,它提供了对标准Java I/O API的替代方案,以支持更高效的文件和网络数据传输。
在JDK 1.7 版本中对NIO进行了完善,推出了NIO.2,也称为AIO(异步IO),在处理大量并发请求时具有优势,特别是在网络编程和高并发场景下,表现得更为出色。
在这里插入图片描述
IO和NIO的区别
Java IO和NIO的主要区别在于两者的处理方式不同。Java IO是面向流(Stream)的,它将输入输出数据直接传输到目标设备或文件中,以流的形式进行读写;而NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
特别是在网络编程和高并发场景下,Java NIO表现得更为出色。Java IO在进行网络通信时,每个客户端连接都需要创建一个线程来进行处理,这样会导致系统资源的浪费。Java NIO则只需要一个线程就可以完成对多个客户端连接的处理,大大减少系统资源的占用。
在这里插入图片描述
二、NIO核心原理
主要包括:缓冲区(Buffer)、通道(Channel)和选择器(Selector)、字符集(Charset);首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。 NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。

  1. 缓冲区Buffer
    缓冲区是Java NIO中一个非常重要的概念,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,用于保存读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。
    缓冲区可以在内存中创建,并可以通过通道(Channel)进行读写操作,也可以作为参数传递给其他方法。缓冲区在java NIO中负责数据的存取,底层缓冲区其实就是数组,用于存储不同数据类型的数据,根据不同的数据类型(Boolean除外),提供了相应类型的缓冲区:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。

缓冲区的四个核心属性:
capacity:容量,表示缓冲区的最大容量,声明后不能改变
position:表示缓冲区中正在操作数据的位置。当前位置,下一个要被读取或写入的位置;
limit:界限,缓冲区中可以操作数据的大小,表示可以读写的元素数量;
mark:标志,可以让缓冲区记住一个position或limit的值。可以通过调用reset()恢复到mark的位置。
缓冲区的读写操作都会修改position和limit属性,例如在从缓冲区中读取数据时,position属性会自动向后移动,而limit属性则不会更改,因此读取操作只能读取到limit位置之前的数据。

四者的关系:0<mark<=position<=limit<=capacity
缓冲区的三个核心操作方法:
put():存数据到缓存区,写数据模式。
flip():切换到读数据模式(position和limit改变,capacity不变)
get():从缓冲区中读取数据。

通过:static ByteBuffe allocate(int capacity)创建指定大小的缓冲区,在JVM内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JJVM内存开销,处理过程中有复杂的操作。

  1. 通道Channel
    通道(Channel)是Java NIO的核心概念,是网络或文件IO操作的抽象,它表示一个数据通讯的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。在java NIO中Channel本身不负责存储数据,通道可以和缓冲区一起使用,让数据直接在缓冲区之间进行传输。

在这里插入图片描述

通道类似于标准IO中的输入输出流,但通道更加灵活和高效。与输入输出流不同的是,通道可以使用Selector选择器实现非阻塞IO操作,并且可以同时进行读写操作。

通道的主要实现类:
FileChannel:用于文件读写操作;
DatagramChannel:用于UDP协议的网络通信;
SocketChannel:用于TCP协议的网络通信;
ServerSocketChannel:用于监听TCP连接请求。

通道的获取方式
java针对支持通道的类提供了getChannel()方法。
支持通道的类如下:
(一)提供本地文件IO的Channel类有:
FileInputStream
FileOutputStream
RandomAccessFile
(二)提供网络套接字IO的Channel类:
DatagramSocket
Socket
ServerSocket
(三)获取通道的其他方式:
在JDK7.0中的AIO针对各个通道提供静态方法open()可打开并返回指定通道;
在JDK7.0中的AIO的Files类可使用Files类的静态方法newByteChannel()获取字节通道。

在使用NIO进行网络编程时,我们常常使用SocketChannel和ServerSocketChannel来实现客户端与服务器之间的通信。使用FileChannel可以完成对本地文件的读写操作,使用DatagramChannel可以发送和接收UDP协议的数据包。

我们来看一个从文件读数据的例程,其中方法fileReadBIO()是用BIO的输入流方式读文件信息;方法fileReadNIO()使用NIO的FileChannel和字节缓冲区ByteBuffer来读文件信息。

package nio;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileReadTest {public static void fileReadBIO(){InputStream in = null;System.out.println("BIO模式读文件测试***");try{String path = "D:/temp/TestBIO.txt";in = new BufferedInputStream(new FileInputStream(path));byte [] buf = new byte[1024];int bytesRead = in.read(buf);while(bytesRead != -1){for(int i=0;i<bytesRead;i++)System.out.print((char)buf[i]);bytesRead = in.read(buf);}}catch (IOException e){e.printStackTrace();}finally{try{if(in != null){in.close();}}catch (IOException e){e.printStackTrace();}}}public static void fileReadNIO(){System.out.println("NIO模式读文件测试***");RandomAccessFile aFile = null;try{aFile = new RandomAccessFile("D:/temp/TestNIO.txt","rw");FileChannel fileChannel = aFile.getChannel();ByteBuffer buf = ByteBuffer.allocate(1024);int bytesRead = fileChannel.read(buf);System.out.println(bytesRead);while(bytesRead != -1){buf.flip();while(buf.hasRemaining()){System.out.print((char)buf.get());}buf.compact();bytesRead = fileChannel.read(buf);}}catch (IOException e){e.printStackTrace();}finally{try{if(aFile != null) aFile.close();}catch (IOException e){e.printStackTrace();}}}public static void main(String[] args) {fileReadBIO();System.out.println();fileReadNIO();}}
  1. 选择器Selector和选择键SelectionKey
    选择器(Selector)和选择键(SelectionKey)是Java NIO提供的另外两个核心组件。选择器用于检测通道的状态,并且可以根据通道状态进行非阻塞选择操作。而选择键则是一种将通道和选择器进行关联的机制。

使用选择器可以实现单线程管理多个通道的方式,以此实现高并发IO操作。在选择器的模型中,每个通道都会注册到一个选择器上,并且每个通道都有一个其唯一的选择键对象来代表这个通道。选择键对象包含几个标志位,表示通道的当前状态等信息。

选择器可以监听多个通道的事件,例如连接就绪、读取数据就绪、写入数据就绪等等。当有一个或多个通道的事件就绪时,选择器就会自动返回这些通道的选择键,我们可以通过选择键获取到对应的通道,然后进行相应的操作。

选择器是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。

  1. 字符集Charset(编码解码)
    编码(按指定Charset编码方案编码)
    字符串转成字节数组
    解码(按指定Charset编码方案解码)
    字节数组转成字符串

请看一个编码和解码的演示例程:

package nio;import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Map;
import java.util.Set;/*** 字符集(Charset)* 编码:字符串-->字节数组* 解码:字节数组-->字符串*/public class CharsetDemo {static String infStr = "绿水青山就是金山银山!";public static void charSetEncoderAndDecoder() throws CharacterCodingException {Charset charset=Charset.forName("UTF-8");//1.获取编码器CharsetEncoder charsetEncoder=charset.newEncoder();//2.获取解码器CharsetDecoder charsetDecoder=charset.newDecoder();//3.获取需要解码编码的数据CharBuffer charBuffer=CharBuffer.allocate(1024);charBuffer.put(infStr);charBuffer.flip();//4.编码ByteBuffer byteBuffer=charsetEncoder.encode(charBuffer);System.out.println("编码后:");for (int i=0;i<byteBuffer.limit();i++) {System.out.println(byteBuffer.get());}//5.解码byteBuffer.flip();CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);System.out.println("\n解码后:");System.out.println(charBuffer1.toString());System.out.println("\n使用不正确的编码格式解码,解码结果:");Charset charset1=Charset.forName("GBK");byteBuffer.flip();CharBuffer charBuffer2 =charset1.decode(byteBuffer);System.out.println(charBuffer2.toString());}/***查询系统可用的字符编码***/public static void getAvailableCharsets() {//6.获取Charset所支持的字符编码System.out.println("\n系统可用的字符编码:");Map<String ,Charset> map= Charset.availableCharsets();Set<Map.Entry<String,Charset>>  set=map.entrySet();for (Map.Entry<String,Charset> entry: set) {System.out.println(entry.getKey()+"="+entry.getValue().toString());}}public static void main(String[] args) throws IOException {/****字符集编码和解码演示****/charSetEncoderAndDecoder();/***查询系统可用的字符编码***/getAvailableCharsets();}
}

下面我们对通道的主要实现类来进行一下介绍:

  1. FileChannel:用于文件读写操作;

文件通道FileChannel是用于读取,写入,文件的通道。FileChannel只能被InputStream、OutputStream、RandomAccessFile所创建。
使用fileChannel.transferTo()可极大提高文件的复制效率,为进行大容量文件的读和写,直接把读通道和写通道建立了连接,还能有效避免因文件过大而导致内存溢出。

FileChannel的常用方法:
int read(ByteBuffer dst) 从Channel当中读取数据至ByteBuffer
long read(ByteBuffer[] dsts)将channel当中的数据“分散”至ByteBuffer[]
int write(Bytesuffer src)将ByteBuffer当中的数据写入到Channel
long write(ByteBuffer[] srcs)将Bytesuffer[]当中的数据“聚集”到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中

这里我们提供一个文件读、写和拷贝复制的例程:

package nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {public static void readFile(){ //读文件例程try {//1.定义一个文件字节输入流与源文件接通FileInputStream fos = new FileInputStream(new File("D:/temp/test01.txt"));//2.获取文件字节输入流的文件通道FileChannel channel = fos.getChannel();//3.定义一个缓存区ByteBuffer buf = ByteBuffer.allocate(1024);int bytesRead = channel.read(buf); //4.读取数据到缓存区String str = null;while(bytesRead != -1) {//5、切换buf.flip();//6.读取缓存区中的数据并输出即可str = new String(buf.array(), 0, buf.remaining());System.out.println("读取内容..." + str);bytesRead = channel.read(buf); //4.读取数据到缓存区}channel.close();    } catch (Exception e) {throw new RuntimeException(e);}}public static void writeFile(){ //写文件例程try {//1.字节输出流通向目标文件FileOutputStream fos = new FileOutputStream(new File("D:/temp/test01.txt"));//2.得到字节输出流对应的通道ChannelFileChannel channel = fos.getChannel();//3.分配缓存区ByteBuffer bf = ByteBuffer.allocate(1024);bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());//4.把缓存区切换为写模式bf.flip();//5.输出数据到文件channel.write(bf);channel.close();System.out.println("完成数据写入....");} catch (Exception e) {throw new RuntimeException(e);}}public static void copyFile(){ //复制文件例程try {long starTime = System.currentTimeMillis();//1、创建输入文件流FileInputStream fis = new FileInputStream(new File("D:/temp/test01.txt"));//2、得到输入channelFileChannel fisChannel = fis.getChannel();//3、创建输出文件流FileOutputStream fos = new FileOutputStream(new File("D:/temp/test02.txt"));//4、得到输出channelFileChannel fosChannel = fos.getChannel();//5、使用输入channel将文件转到fosChannelfisChannel.transferTo(0, fisChannel.size(), fosChannel);fis.close();fos.close();fisChannel.close();fosChannel.close();long endTime = System.currentTimeMillis();System.out.println("耗时=" + (endTime - starTime) + "ms");} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {writeFile(); System.out.println("从文件读信息:");readFile();}
}

例程说明:
方法readFile()演示如何从文件读入数据信息;
方法writeFile()演示如何把数据信息写入磁盘文件中。
方法copyFile()则是一个文件复制演示程序。
由于例程中没有显式指定数据信息的字符集编码方案,如果读入一个其他文本编辑器编辑的文本文件,显示出来也可能是乱码的。
因此,本例程中最后测试时,先调用writeFile(); 然后再调用readFile();,这样可确保读写测试都使用默认的字符集编码方案。由于写入的文本信息太少,第一次无法测试到循环读信息的效果。可在第一次测试后,再用文本编辑器随意增加足够文本信息,再进行测试,才可测试到循环读信息。

缓冲区Buffer的使用说明:
Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。通道Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须在缓冲区Buffer进行缓存。
可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态:capacity, position, limit, mark:

在这里插入图片描述
我们以“写文件例程”方法writeFile()为例来进行说明。我们来分析这几行源代码:

            //3.分配缓存区ByteBuffer bf = ByteBuffer.allocate(1024);bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());//4.把缓存区切换为写模式bf.flip();//5.输出数据到文件channel.write(bf);channel.close();

//3.分配缓存区
ByteBuffer bf = ByteBuffer.allocate(1024);
当执行完上面这行代码后,程序分配了缓冲区Buffer,此时Buffer的初始状态如下图,position的位置为0,capacity和limit默认都是数组长度。
在这里插入图片描述
bf.put(“最近公司有个需求,就是上传产品详情图。” .getBytes());
执行完上面这行代码,缓冲区Buffer写入了数据后,position的位置移到写入数据的后面,缓冲区的状态如下:
在这里插入图片描述
//4.把缓冲区切换为写模式
bf.flip();
执行完上面这行代码flip()后,缓冲区的position的位置移到0,limit则移到了原来position的位置。
在这里插入图片描述
在下一次再往Buffer写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
调用clear()方法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”。
如果Buffer中仍有未读的数据,且后续还需要这些数据,那么可使用compact()方法,其功能有点像磁盘碎片整理的作用。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position,之后可以通过调用Buffer.reset()方法恢复到这个position。
Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。

关于NIO网络通讯编程的应用请参见博客:
Java 输入与输出之 NIO【非阻塞式IO】【NIO网络编程】探索之【二】

参考文献&博客:

  1. 参考文献之一
    攻破JAVA NIO技术壁垒
  2. 参考文献之二
    Java NIO全面详解(看这篇就够了)
  3. 参考文献之三
    Java NIO详解
  4. 参考文献之四
    Java FileChannel文件的读写实例

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • C语言——字符函数、字符串函数和内存函数
  • 计算机网络面试真题总结(六)
  • QT 与 C++实现基于[ TCP ]的聊天室界面
  • led护眼台灯对眼睛好吗?台灯护眼是真的吗?一文告诉你答案
  • 单例模式在实现webserver这个项目中起到了什么作用
  • 如何完美备份自己的微博,即使是封号之后
  • 【北森-注册安全分析报告-无验证方式导致安全隐患】
  • Ubuntu系统使用Docker部署中文版trilium并实现远程编辑笔记
  • 游戏+AI
  • 人大金仓数据库常见运维方式整理
  • 视频压缩工具大PK:四款神器让你轻松压缩不卡顿
  • Mysql系列—4.Mysql安装
  • Python中csv文件的操作3
  • PyQt 迁移到 PySide
  • 二十三种模式之单例模式(基础了解)
  • 【Amaple教程】5. 插件
  • Angular 响应式表单之下拉框
  • EOS是什么
  • ES学习笔记(12)--Symbol
  • JavaScript的使用你知道几种?(上)
  • Java多态
  • k个最大的数及变种小结
  • Laravel 菜鸟晋级之路
  • Laravel5.4 Queues队列学习
  • 将回调地狱按在地上摩擦的Promise
  • 聚类分析——Kmeans
  • 配置 PM2 实现代码自动发布
  • 如何在 Tornado 中实现 Middleware
  • 深入浏览器事件循环的本质
  • 使用Swoole加速Laravel(正式环境中)
  • 学习HTTP相关知识笔记
  • 学习使用ExpressJS 4.0中的新Router
  • Play Store发现SimBad恶意软件,1.5亿Android用户成受害者 ...
  • 进程与线程(三)——进程/线程间通信
  • # 数据结构
  • #define 用法
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • (1)无线电失控保护(二)
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (LLM) 很笨
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (附源码)python旅游推荐系统 毕业设计 250623
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (五)关系数据库标准语言SQL
  • (一)python发送HTTP 请求的两种方式(get和post )
  • (一)搭建springboot+vue前后端分离项目--前端vue搭建
  • (原创)boost.property_tree解析xml的帮助类以及中文解析问题的解决
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • (转)shell调试方法
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • .axf 转化 .bin文件 的方法
  • .net core 依赖注入的基本用发