原文链接:https://blog.csdn.net/finalheart/article/details/86590029
NIO的简介以及与普通IO的区别
首先说一下这个NIO 是从jdk1.4版本之后开始有的。这个出现的目的就是提升IO的速度以及节省资源,以至于更高效的处理代码。NIO和IO都是Java的rt.jar中的。 下面说一下两者的区别。
IO又称为阻塞IO,NIO是no阻塞IO(非阻塞IO)。
IO是面向流的,而NIO是面向缓冲区的。
NIO有一个selector的概念,就是可以选的执行。
对于IO来说,一个用户连接会产生一个线程。对于NIO来说。多个用户连接可以用一个线程来搞定。
对于NIO与IO的应用场景:NIO适合并发高,连接资源多的场景。IO适合并发低,连接数量少。文件传输。一般网络之间的大量连接用的就是NIO。比如多人在线与客服聊天。多个连接就是NIO的。
NIO与IO的区别
(1) 下面是各种流的read方法。可以看到他们都在read的时候用synchronized关键字把这个方法或者方法体锁住了。对于fileInputStream调用底层的native就不多说了。write()方法的做法跟read一样,都是锁住了。意味着我们多个用户访问的时候这个是阻塞执行的。对于多个用户访问就产生了多个线程。而且在用户量大的时候我们还需要切换这个线程。取决于操作系统的调度算法。但是局限在没有高的并发时。虽然你执行完IO多余的线程可以关闭。但是这并不影响你效率低。当并发高一点。得造一堆线程。就算你用docker起一堆镜像也难以解决这个问题。
对于阻塞与非阻塞。阻塞就是一直等,直到有资源可以消费。非阻塞就是没资源 我返回没资源,然后我不等。
(2) 传统IO有个bufferedReader可以缓存,NIO有个Buffer缓冲区来缓存。BufferedReader是流读进来的,Buffer对于操作系统来说相当于一个内存块。在我理解的是这样的。
IO:文件——-stream流——-BufferedReader NIO:文件———channel通道———-buffer内存块缓冲区。
而NIO较IO的优势就是这个channel和buffer 这俩东西跟操作系统的交互更加快。
(3) 对于NIO来说有一个selector这个可以监听到多个通道的状态。这个多个通道可以理解为一个通道是一个read,write。所以这句话可以理解为一个线程调selector然后查不同用户通道的状态。免去了阻塞的问题。减少了线程数。提高了效率。
对于NIO,一次读取数据的流程可以抽象成:
1.获取到通道。
2.创建buffer缓冲区。
3.打开读取开关。
4.读取数据。
5.清空缓冲区。
NIO还有一个缺点,每一次的数据处理都是对缓冲区进行的,以至于在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,所以每次数据处理之前都要检测缓冲区数据。buf.hasRemaining();
通道
这个channel通道就相当于stream流似的。最顶层是一个接口,有几个实现类。以fileChannel为例子。selector再看其他通道。
FileChannel:文件通道,用于文件的读和写。 本文截止到selector的介绍主要用这个通道做例子。
DatagramChannel:用于 UDP 连接的接收和发送
SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求
流是单向操作的。输入流输出流。 还有自成一派的RandomAccessFile
channel通道是双向操作的。就是用Buffer能读取也能写。
下面是我的一个小例子。下文会慢慢写啥啥啥都是啥,都是咋用的。
创建FileChannel的方式有上图中的那三种。FileChannel其实只是个abstract的傀儡,实际干事的是FileChannelImpl.class
上图中用到的position方法就相当于获取文件下标的位置。这个channel是read方法装到Buffer,我们才能使用的。
下面是channel的read扔到buffer的方法。可以看到挺麻烦的。这个channel通道的源码挺复杂的。我也不太会,不多说了。
public int read(ByteBuffer var1) throws IOException {
this.ensureOpen();
if (!this.readable) {
throw new NonReadableChannelException();
} else {
Object var2 = this.positionLock;
synchronized(this.positionLock) {
int var3 = 0;
int var4 = -1;
byte var5;
try {
this.begin();
var4 = this.threads.add();
if (this.isOpen()) {
do {
var3 = IOUtil.read(this.fd, var1, -1L, this.nd);
} while(var3 == -3 && this.isOpen());
int var12 = IOStatus.normalize(var3);
return var12;
}
var5 = 0;
} finally {
this.threads.remove(var4);
this.end(var3 > 0);
assert IOStatus.check(var3);
}
return var5;
}
}
}
对于read和write方法我的理解是下图。这个东西就像吃自助餐一样。最后可能剩下(capacity-limit)
channel通道之间也是可以转换的。下面是将输入流变成输出流写进out.txt。
Buffer
相当于一块封装的内存块。可以读写。配合channel使用。下面看看源码。下面是Buffer这个类的子类信息。
下图为Buffer的一些重要的实例变量。buffer的方法玩的就是这几个变量。
mark
这个是跟reset一起使用的,目的就是把当前位置标记。可以看到是final的方法,真好。
可以看到。mark刚开始是 -1 我们标记就置为position。reset的时候就把position设置成mark的值。这个就是标记版的position(int);方法。
position
这个就是即将被使用的数组中的位置。0为起始下标。下图为获取当前buffer实例的position。设置position位置
position的位置不能高于limit限制区的位置。就像我们这个Buffer没装满。他只能读到装的最后一块。不能再多了。channel也有这个position的方法,注意这是两个不一样的东西。一个是缓冲区这个小范围的。一个是channel相当于文件这个大范围的。
limit
这个是限制区的意思。它是限制position用的。也就是buffer可用大小限制。他的区间是 [0,capacity]。他是没有设置默认值的。构造函数给他的初始化是capacity大小。
buffer有如下两个方法获取和设置limit位置。源码第274行就是他的范围了。mark和position是不能大于他的。下面的源码很简单的解释我这些话了。
下面用ByteBuffer来介绍一下limit的初始化过程。此处假设我们使用初始Buffer的方法是allocate()。三个图结合看。
可以看到我们初始化的时候new了一个HeapByteBuffer对象,这个对象初始化是用ByteBuffer来初始化的。并把limit位置和capacity位置都设置为capacity的大小了。所以我说的他的构造初始值为容量大小。
capacity
就是它单词的意思: 容量。在我们初始化一个Buffer的时候,我们会给这个Buffer大小。例如allocate(int capacity)方法。这个大小就是capacity。
下面的方法是取capacity值的大小。
以上的这些Buffer类中的方法几乎都是final的。因为abstract的我还没说呢。
flip
接下来我们来看一下这个反转方法,也就是我之前注释里面的读数据开关方法。
可以看到,他把limit设置成当前位置了,把当前位置置0,抛弃mark标记。并返回当前实例。
buffer.hasRemaining就是判断 position < limit 不。小于则说明此次的buffer没遍历完呢,就是没有get取完。
对于buffer来说。他要不断地挪position的位置来做到读取的目的。下面是取值的get方法。就是position+1取索引
clear
clear方法以及作用。
上述的这些是一个流程下来的操作样式。也就是操作position和limit。对于Buffer这个类还有下面的方法。
还有一些不直接常用的方法和抽象方法就不粘贴出来了。像玩mark的方法和nextposition位置的方法等等。
Buffer ———-ByteBuffer ————HeapByteBuffer 这么个继承顺序。
Buffer的创建方式
下面是buffer的构造方法。byteBuffer就是在抽象类buffer的基础上加字节数组。这么一结合才是玩索引的。只有ByteBuffer直接与channel相连。其他的像CharBuffer 得转成Buffer再与channel玩。
allocate入参一个capacity容量大小来设置内存块的大小,这个一般是channel需要read的时候这么创建一个Buffer
put
这个是往channel写东西时候可以用的。你愿意干别的也行。看下面的代码。put实际上是数组指定索引的拷贝。length太长的话 position(int)就不惯你脾气直接抛异常了。
tips: put的时候,如果缓冲区Buffer的容量大于你要写入的数据的话。是会其他位补默认值的。因为数组初始化是有默认值的。
可以看到有下面这么多的put的类型。是实现类HeapByteBuffer调用底层的方法来放进去的。
compact
虽然这两个方法的结果看上去一样。但是实际动作clear少了未读完数据覆盖的那步。
作用是把缓冲区positoin到limit中的元素向前移动positoin位。把position设置为remaining()。limit为缓冲区容量。最后取消mark标记。我们nio的操作是非阻塞的。所以不一定会一次行把buffer中的数据全解决掉。compact方法相当于将把position 与 limit之间这次没读完的数据复制到buffer的开始0位置。这样我们这次没读完下次读还是刚刚好的那个位置。不会被覆盖掉。而clear是position指向0了。也就是说这次要是没读完就不管了。下次position直接从0走覆盖掉。所以要尽量使用compact代替clear防止数据被覆盖。
rewind: position置0,但是没动limit的位置。所以下一次读的时候只能读[position,limit)的数据、
clear: position置0,limit=capacity。相当于把这个buffer彻底”清空”因为下次read会从position开始覆盖掉之前buffer的数据。
compact: 先把本次剩余没读的装起来。在read覆盖buffer的时候从上一次未读的数据之后的索引开始写值。limit=capacity。1级棒。
实现comparable接口,可排序
下面是重写的compareTo方法。可以看到先是安装buffer里面的数据排序。因为buffer里面都是基本类型。相当的话最后返回的是剩余数据数量的比较。
MapedByteBuffer
Buffer的开始,说过buffer有多个直接实现类。包括七种(除了布尔)的基本类型的包装版Buffer。当然还有MapedByteBuffer。
下面说一下他们之间的关系。
其实与通道直接相连的就是ByteBuffer.拿read方法举个例子。根本没有ByteBuffer之外的刚才说的基本类型包装版buffer的事。
所以我们在读数据的时候可以使用其他Buffer来替代ByteBuffer读取。来代替一个一个get的方式读取。而写的时候必须是ByteBuffer往里写。读取的时候ByteBuffer转成其他Buffer。写的时候也是同样道理。
例如CharBuffer来读取。但是这有个charset字符集编码的问题。下图是例子。
其他的基本数据类型的包装Buffer就不做介绍了。没仔细看。
还有就是Buffer数组可以被channel操作。意味着channel可以读数据到Buffer数组里面。一个满了再读进下一个。写同理。但是相对的内存消耗就更大些。个人感觉使用场景较少。
selector
选择器selector。这个的目的就是用更少的线程管理更多的连接/通道。以上的channel介绍都是使用的fileChannel。但是fileChannel有一个问题。那就是他是不能设置为非阻塞的。他没有继承这个SelectableChannel。fileChannel这个通道只能是阻塞的方式玩。所以filechannel也就不能往selector中注册。
下面这个方法是设置当前通道是否为非阻塞。
channel通道需注册到selector里才能一起使用。对于socketChannel。要跟serverSocketChannel一起玩。这个socketChannel相当于其中客户端。serverSocketChannel 则表示的是一个服务端。下面是把通道注册到selector选择器中。
这个事件注册之后我们可以根据这个selectionKey获取到所注册的事件。然后愿意咋操作咋操作。这个设置成哪个就是那个状态就绪。23行是要连接的地址。24行的 那个SelectionKey一共有四个事件。此处用的是OP_READ。
下面介绍的是SelectionKey中的interest
下面那个int值就是对应的数。多个事件监听的话就把值加起来就行。比如 write和read一起。就是5.当然OP_WRITE | OP_READ这样写更严谨。下面是24行debug进去的实际调用代码。
这个SelectionKey是一个起到承上启下作用的一个类。这个类很关键。下面是获取当前key的interest集合。也就是净注册啥事件了。本例是5也就是读和写就绪。最后输出i为5
selectionkey.interestOps()方法获取的就是我们当时设置连接时设置的感兴趣的事件。本例返回一个5就是读和写,这样看起来更通俗。
selectionkey.readyOps()方法获取的是现在已经就绪的事件。
在使用register向选择器注册的时候,还可以附加一个Object的attachment对象。这个可以作为标识啥的用。在从选择器取得时候可以根据这个attachment对象来获取。
在生成的key也可以定义attachment 会将之前定义的覆盖掉。
selectionKey还能获取到当前key的channel和selector
下面是selector选择器选择的方法。
select是阻塞的方法。直到这个选择器上面的通道有一个就绪。这个就绪就是当时设置的那个interest的ready有显示值了。
select(timeout);就是阻塞多久。时间到没有通道进入就绪就返回0。
selectNow是当前就绪的通道有多少个。没有就返回0。
下面这个方法可以获取到所有向这个selector注册的selectionKey且有通道的interest事件就绪的。
Set
对于以上select阻塞的场景。如果我们想主动让程序跳出阻塞。可以另起一个线程使用selector.wakeup方法。打破阻塞。在没有阻塞的情况下使用这个方法。下个select会立即返回。
关于socketChannel和serverSocketChannel的例子 文末的参考链接有例子。
https://www.cnblogs.com/snailclimb/p/9086334.html