本篇将分为BIO、NIO概念介绍、javaNIO组件、并发NIO简易模型实例三部分组成,为读者和我自己增加对NIO的理解。
这里主要是介绍NIO,但是在介绍NIO的时候难免会提到BIO,所以这里也顺带介绍一下什么是BIO,由此切入NIO。
在介绍BIO之前先来了解一下更加普遍的概念IO,IO是输入流和输出流的统称,在IO的世界里,有几个重要的概念,分别是同步、异步、阻塞、非阻塞,在这一小部分主要也是介绍这四个的概念。
同步与异步
同步异步中有一个主线程的概念问题,就是当前处理逻辑的线程,假设一种场景,在主线程中依次执行方法一、方法二
那么同步就是在调用方法一时,方法二必须等方法一调用返回后才能继续执行后续的行为。顺序执行
异步就是在调用方法一时,立即返回,主线程不需要等待方法一内部代码执行完成,就可以继续执行方法二。并行执行,方法一交给了另外一个线程去处理,不由主线程执行,因此也不会阻塞主线程。
阻塞与非阻塞
在单个线程内遇到同步等待时,是否在原地不做任何操作
阻塞是指遇到同步等待后,一直在原地等待同步方法处理完成;
非阻塞是指遇到同步等待后,不在原地等待,先去处理其他的操作,隔段时间再来判断同步方法是否完成
举个老王烧水的例子
同步阻塞(BIO): 老王烧水,老王需要在水壶旁一直等待水烧开才能去做其他事情;
同步非阻塞(NIO): 老王烧水,老王将水放入水壶并且开始烧水后,可以先去做其他事情,但是每隔一段时间就去查看一下水是否烧开了;
异步非阻塞(AIO): 老王烧水,老王开始烧水后直接去做其他事情,水壶会在水烧开后自动鸣笛通知老王继续执行下一步操作。
Blocking IO 是JDK1.4之前的传统IO模型,本身是同步阻塞模型,线程发起IO请求后,会一直阻塞IO直到缓存区数据就绪后,再进入下一步的操作。针对于网络通信都是一个请求一个应答的方法,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。
对于BIO的资源方面的优化,最早是使用线程池来管理IO响应线程,最大限度上的节约线程方面的消耗,但是这种方面依然不能避免高并发情况下的线程损耗问题。
而这也是NIO诞生的原因,任何技术诞生都是为了解决某个已知的问题的。
Non-blocking IO是在jdk1.4被提出,同步非阻塞模型,线程发起IO请求后立即返回。同步指的是必须等待IO缓冲区内的数据就绪,而非阻塞指的是,用户线程不必原地等待IO缓冲区,可以先做一些其他操作,但是要定时轮询检查IO缓冲区数据是否就绪。
由此来看,BIO模型中每个线程都可以处理多个请求,这样就可以解决资源消耗严重问题。但是因此可以想到一个线程处理多个请求时,如果出现请求过多,那么必然会出现请求堆积排队处理,那么响应的速度肯定会下降,这个也是NIO的弊端,相较于这点BIO是响应速度最快的,因为它是一直在等待响应。
在并发下,NIO的弊端并不是不可以避免,这就要看采用的BIO模型是什么样的了,合理一些的应该根据服务器的CPU内核、内存等分配一些线程,多线程的方式处理各个NIO 多路复用器的事件。
在BIO和NIO之间如何选择?
BIO和NIO同时存在他们的优缺点,并不是一定NIO一定优于BIO,这样形容就有些片面了。如何选择要看业务定义和资源的支持情况,从这里也可以看出一个二八原则,是否要为百分之二十的响应时间而浪费百分之百的线程资源。
关于什么是AIO的问题,研究不多,只引入概念
AIO,Asynchronous IO,在进行 I/O 编程中,通常用到两种模式:Reactor 和 Proactor 。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 叫做异步非阻塞
的 I/O,引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才会启动线程,特点就是先由操作系统完成后才通知服务端程序启动线程去处理,一般用于连接数较多且连接时长较长的应用。
可以理解有一个回调函数,当有I/O请求并已经准备好时,操作系统直接调用回调函数完成请求,由此可以看出这个AIO是依赖于操作系统的。
java中NIO的三个组件,分别是Channel、buffer、selector。
NIO
使用信道 (Channel
) 来发送和接收数据,而不使用传统的流 (InputStream/OutputStream
)。
Channel
实例代表了打开一个实体的连接,这些实体包括硬件设备,文件,网络套接字等等。 Channel
有个特色,在 Channel
上的操作,例如读写,都是线程安全的。
SelectableChannel
是一个抽象类,它实现了 Channel
接口,这个类比较特殊。
首先 SelectableChannel
可以是阻塞或者非阻塞模式。如果是阻塞模式,在这个信道上的任何 I/O 操作都是阻塞的直到 I/O 完成。 而如果是非阻塞模式,任何在这个信道上的 I/O 都不会阻塞,但是传输的字节数可能比原本请求的字节数要少,甚至一个也没有。这里主要是指NIO非阻塞立即返回结果,所以读取的数据有多有少。
其次呢 SelectableChannel
可以被 Selector
用来多路复用,不过首先需要调用 selectableChannel.configureBlocking(false)
调整为非阻塞模式(nonblocking mode
),这一点很重要。然后进行注册
SelectionKey register(Selector sel, int ops)
SelectionKey register(Selector sel, int ops, Object att)
第一个参数代表要注册的 Selector
实例。关于 Selector
后面再讲。
第二个参数代表本通道感兴趣的操作,这些都定义在 SelectionKey
类中,如下
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
对于 SocketChannel
,它感兴趣的操作只有 OP_READ
, OP_WIRTE
和 OP_CONNECT
,然而它并不包括 OP_ACCEPT
。 而 ServerSocketChannel
可以对这四个操作都感兴趣。为何?因为只有 ServerSocketChannel
有 accpet()
方法。
DatagramChannel
、SocketChannel
、ServerSocketChannel
都是SelectableChannel
的子类。
第三个参数 Object att
是注册时的附件,也就是可以在注册的时候带点什么东西过去。这里在后续也可以使用SelectionKey的attach方法
register()
方法会返回一个 SelectionKey
实例。SelectionKey
相当于一个 Java Bean
,其实就是 register()
的三个参数的容器,它可以返回和设置这些参数
Selector selector();
int interestOps();
Object attachment()
**FileChannel:**从文件中读写数据。
DatagramChannel: 通过UDP读写网络中的数据,继承自SelectableChannel。
**SocketChannel:**通过TCP读写网络中的数据,继承自SelectableChannel。
**ServerSocketChannel:**可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel,继承自SelectableChannel。
ServerSocketChannel和SocketChannel在下面的实例中会用到,他们是基于TCP进行连接,这里也着重介绍下
ServerSocketChannel
类代表服务器端套接字通道(server-socket channel
)。
ServerSocketChannel
和 SocktChannel
一样,需要通过静态方法 open()
来创建一个实例,创建后,还需要通过 bind()
方法来绑定到本地的 IP 地址和端口
ServerSocketChannel bind(SocketAddress local)
ServerSocketChannel bind(SocketAddress local, int limitQueue)
参数 SocketAddress local
代表本地 IP 地址和端口号,参数 int limitQueue
限制了连接的数量。
对每一个新进来的连接都会创建一个SocketChannel去管理这个连接
SocketChannel
代表套接字通道(socket channel
)。
SocketChannel
实例是通过它的静态的方法 open()
创建的
open()
方法仅仅是创建一个 SocketChannel
对象,而 open(SocketAddress remote)
就更进一步,它还调用了 connect(addr)
来连接服务器。
SocketChannel
是 SelectableChannel
的子类,还记得前面 SelectableChannel
的特性吗?如果不配置阻塞模式,那么 SocketChannel
对象默认就是阻塞模式,那么 open(SocketAddress remote)
方法其实就是阻塞式打开服务器连接。而且在 SocketChannel
上任何 I/O 操作都是阻塞式的。
那么既然 SelectableChannel
可以在非阻塞模式下的任何 I/O 操作都不阻塞,那么我们可以先调用无参的 open()
方法,然后再配置为非阻塞模式,再进行连接,而这个连接就是非阻塞式连接,伪代码如下
// 创建 SocketChannel 实例
SocketChannel sc = SocketChannel.open();
// 调整为非阻塞模式
sr.configureBlocking(false);
// 连接服务器
sr.connect(remoteAddr);
此时的 connect()
方法是非阻塞式的,我们可以通过 isConnectionPending()
方法来查询是否还在连接中,如果还在连接中我们可以做点其它事,而不用像创建 Socket
一样一起阻塞走到连接建立,在这里我们可以看到使用 NIO
的好处了。
如果 isConnectionPending()
返回了 false
,那就代表已经建立连接了,但是我们还要调用 finishConnect()
来完成连接,这点需要注意。
关键的Buffer实现 ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer
Buffer两种模式、三个属性:
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1. 当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. limit会置为写模式中position的位置,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
缓存区基础:https://www.cnblogs.com/chenpi/p/6475510.html
Selector
是 SelectableChannel
的多路复用器,可以用一个 Selector
管理多个 SelectableChannel
。例如,可以用 Selector
在一个线程中管理多个 ServerSocketChannel
,那么我们就可以在单线程中同时监听多个端口的请求,这简直是美不可言。 从这里我们也可以看出使用 NIO
的好处。
Selector
实例也需要通过静态方法 open()
创建。
前面说过,我们需要调用 SelectableChannel
的 register()
来向 Selector
注册,它会返回一个 SelctionKey
来代表这次注册。
可以通过 Selector
管理多个 SelectableChannel
,它的 select()
方法可以监测哪些信道已经准备好进行 I/O 操作了,返回值代表了这些 I/O 的数量。
int select()
int select(long timeout)
int selectNow()
当调用 select()
方法后,它会把代表已经准备好 I/O 操作的信道的 SelectionKey
保存在一个集合中,可以通过 selectedKeys()
返回。
select()
的三个方法,从命名就可以看出这几个方法的不同之处,第一个方法是阻塞式调用,第三个方法设置了一个超时时间,第三个方法是立即返回。如果是多线程模型,最好使用wakeup去主动唤醒阻塞Selector,否则将响应变慢。
如果调用 selcet()
方法会导致线程阻塞,甚至无限阻塞,wakeUp()
方法是唤醒那些调用 select()
方法而处于阻塞状态的线程。
Selector原理:https://www.iteye.com/blog/zhhphappy-2032893
简易并发模型,使用一个线程管理器来分别管理IO线程和业务处理线程。将NIO中读和写分别在不同的线程进行以避免并发请求过多造成排队问题,将具体业务处理和IO隔离开,以免影响接受IO请求。
以下对于线程池、Selector管理器、读写Selector代码详细介绍,其他内容省略,具体可看GitHub
public class CustomizedThreadPool {
private final static ExecutorService SOCKED_THREAD_POOL = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors()*2,500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
private final static ExecutorService READ_WRITE_THREAD_POOL = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors()*2,Runtime.getRuntime().availableProcessors()*5,500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
public static void sockedHandlerSubmit(Runnable task) {
SOCKED_THREAD_POOL.submit(task);
}
public static void writeHandlerSubmit(Runnable task) {
READ_WRITE_THREAD_POOL.submit(task);
}
public static void shutdown() {
// 先关闭端口监听,不在接受请求
// 处理完剩下所有请求后关闭
SOCKED_THREAD_POOL.shutdown();
READ_WRITE_THREAD_POOL.shutdown();
}
}
线程池这里,我将IO线程和业务线程隔离开了,这是为了不会因为业务处理的问题而导致建立连接超时。
这里有一个细节,就是建立线程池的核心线程数和最大线程数应该如何选取?
这个主要要根据业务来进行区分,可以分为两类,CPU密集型和IO密集型
CPU密集型: CPU利用率比较大,那么应该尽量少的线程数量,一般为CPU的核数+1;
IO密集型: 因为IO阻塞的时间比较多,所以可以多分配一点线程数,公式是:CPU核数/(1-阻塞系数),其中阻塞系数在0.8~0.9之间。
可在java中使用Runtime.getRuntime().availableProcessors()
来查询jvm可使用的CPU核心数
如何判断线程池线程数?
https://blog.csdn.net/weixin_43975771/article/details/113099180
https://juejin.cn/post/6844903990870671374
public class SelectorManagerBuilder {
public static SelectorManager build(int port, int readAndWriteSelectorAccount) throws IOException {
if (readAndWriteSelectorAccount == 0) {
throw new IllegalArgumentException("readAndWriteSelectorAccount 不可以为0");
}
return new SelectorManager(port, readAndWriteSelectorAccount);
}
}
public class SelectorManager {
// 关注read事件的selector的集合
private final static List<Selector> readSelectors = new ArrayList<>();
// 关注write事件的selector的集合
private final static List<Selector> writeSelectors = new ArrayList<>();
private final int PORT;
public SelectorManager(int port, int readAndWriteSelectorAccount) throws IOException {
this.PORT = port;
for (int i = 0; i < readAndWriteSelectorAccount; i++) {
// 初始化写相关多路复用器
Selector writeSelector = Selector.open();
CustomizedThreadPool.sockedHandlerSubmit(new WriteSelector(writeSelector));
writeSelectors.add(writeSelector);
// 初始化读相关多路复用器
Selector readSelector = Selector.open();
CustomizedThreadPool.sockedHandlerSubmit(new ReadSelector(readSelector, writeSelector));
readSelectors.add(readSelector);
}
}
public void startNIO() throws IOException {
// 多路复用器
Selector acceptSelector = Selector.open();
// 服务端通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 设置为非阻塞
ssc.configureBlocking(false);
// 监听本地端口
ssc.bind(new InetSocketAddress(this.PORT));
ssc.register(acceptSelector, SelectionKey.OP_ACCEPT);
int i = 0;
while (true) {
// 返回已经准备好并且感兴趣的selectedKeys数量
if (acceptSelector.selectNow() == 0) {
continue;
}
// 返回已经准备好并且感兴趣的selectedKeys集合
Iterator<SelectionKey> keyIterator = acceptSelector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受客户端请求
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
Selector readSelector = readSelectors.get(i % readSelectors.size());
// 关注Read事件
socketChannel.register(readSelector,SelectionKey.OP_READ);
// 将当前的selectorKey从selectedKeys移除,就不会重复触发accept事件了;
// 除非再次有请求到达触发该强求
keyIterator.remove();
// 唤醒read多路复用器所在线程,减少线程等待时间
readSelector.wakeup();
i++;
if (i == Integer.MAX_VALUE - 1) {
i = 0;
}
}
}
}
}
}
在Selector管理器中预生成若干个读和写的Selector,并将他们分别放入到不同的线程中,进行开始接受状态。这里有一个技术点没有实现,应该是可以根据不同并发情况动态调整Selector所占用的线程数量。
提供StartNIO方法,关注Accept事件开始处理请求
public class ReadSelector implements Runnable{
private final Selector thisSelector;
private final Selector writeSelector;
public ReadSelector(Selector thisSelector, Selector writeSelector) {
this.thisSelector = thisSelector;
this.writeSelector = writeSelector;
}
@Override
public void run() {
while (true) {
try {
// 返回已经准备好并且感兴趣的selectedKeys数量
if (thisSelector.select(1000) == 0) {
continue;
}
// 返回已经准备好并且感兴趣的selectedKeys集合
Set<SelectionKey> selectionKeys = this.thisSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 分配缓存区
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder readData = new StringBuilder();
// 一次性将请求内容全部读取到buffer中,在进行处理和写入操作;一边处理一边处理一边写暂未实现
while (clientChannel.read(buffer) > 0) {
readData.append(new String(buffer.array()));
buffer.clear();
}
CustomizedThreadPool.writeHandlerSubmit(new SelectedServiceHandler(readData.toString(), this.writeSelector, clientChannel));
// 解除该selectionKey和Selector之间的关系,并将它加入到该selector的cancelled-key set中,随后下次Selector操作将这个key从key sets中移除
key.cancel();
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class WriteSelector implements Runnable{
private final Selector thisSelector;
public WriteSelector(Selector thisSelector) {
this.thisSelector = thisSelector;
}
@Override
public void run() {
while (true) {
try {
// 返回已经准备好并且感兴趣的selectedKeys数量
if (thisSelector.select(1000) == 0) {
continue;
}
} catch (IOException e) {
e.printStackTrace();
}
Set<SelectionKey> selectionKeys = thisSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
// 该key的附件,这里放逻辑处理后的返回值
String responseData = String.valueOf(key.attachment());
if (key.isValid() && key.isWritable() && (!isStringEmpty(responseData))) {
SocketChannel clientChannel = (SocketChannel) key.channel();
clientChannel.write(ByteBuffer.wrap(responseData.getBytes()));
}
key.cancel();
keyIterator.remove();
} catch (IOException e) {
e.printStackTrace();
} finally {
key.cancel();
try {
System.out.println("closed.......");
key.channel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
private Boolean isStringEmpty(String data) {
return null == data || "".equals(data);
}
}
在读Selector一次性读取来自客户端的数据,这里也没有一边读一边写的功能,读取到数据后交给业务处理器进行处理,然后再由业务处理器统一调用写Selector将响应结果返回给客户端。
在多线程下,注意要是用wakeup()去唤醒下一个处理线程,否则它也会自己唤醒,但是响应太慢了。
将key从SelectionKey集合中移除后,除非重复发生它感兴趣的事件,否则不会重复触发。
更多细节关注GitHub
不合理处望指正
https://juejin.cn/post/6844903601261936653#heading-3
https://juejin.cn/post/6844903821198508040
https://segmentfault.com/a/1190000037714804
https://juejin.cn/post/6844903985158045703#heading-2
https://www.jianshu.com/p/5bb812ca5f8e