深入了解NIO底层原理

举报
陈皮的JavaLib 发表于 2021/06/09 23:06:01 2021/06/09
【摘要】 Redis 为何能支持高并发? Redis底层采用NIO中的多路IO复用的机制,对多个不同的连接(TCP)实现IO复用,很好地支持高并发,并且能实现线程安全。 Redis官方没有windows版本,只有Linux版本。 NIO在不同的操作系统上实现的方式有所不同,在Windows操作系统使用select实现轮训,而且还存在空轮训的情况,效率非常低。时间复杂度是为O...

Redis 为何能支持高并发?

Redis底层采用NIO中的多路IO复用的机制,对多个不同的连接(TCP)实现IO复用,很好地支持高并发,并且能实现线程安全

Redis官方没有windows版本,只有Linux版本。

NIO在不同的操作系统上实现的方式有所不同,在Windows操作系统使用select实现轮训,而且还存在空轮训的情况,效率非常低。时间复杂度是为O(n)。其次默认对轮训的数据有一定限制,所以难于支持上万的TCP连接。
在Linux操作系统采用epoll实现事件驱动回调,不会存在空轮训的情况,只对活跃的socket连接实现主动回调,这样在性能上有大大的提升,时间复杂度是为O(1)

Windows 操作系统是没有epoll,只有Linux系统才有epoll。

这就是为什么nginx、redis都能够非常好的支持高并发,最终都是Linux中的IO多路复用机制epoll。

阻塞和非阻塞

阻塞和非阻塞通常形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。而非阻塞允许多个线程同时进入临界区。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

BIO NIO AIO 概念

BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

NIO 讲解

我们知道,BIO是阻塞式IO,是面向于流传输也即是根据每个字节实现传输,效率比较低;而NIO是同步非阻塞式的,式面向于缓冲区的,它的亮点是IO多路复用
我们可以这样理解IO多路复用,多路可以指有多个不同的TCP连接,复用是一个线程来维护多个不同的IO操作。所以它的好处是占用CPU资源非常小,而且线程安全。

NIO核心组件

管道channel:数据传输都是经过管道的。channel都是统一注册到Selector上的。
选择器Selector:也可称为多路复用器。可以在单线程的情况下维护多个Channel,也可以维护多个连接。

在这里插入图片描述

BIO 和 NIO 代码演示

传统的BIO阻塞式Socket过程:

先启动一个Socket服务端,此时控制台会输出开始等待接收数据中...,并等待客户端连接。

package com.nobody;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/7/4
 */
public class SocketTcpBioServer { private static byte[] bytes = new byte[1024]; public static void main(String[] args) { try { // 创建ServerSocket final ServerSocket serverSocket = new ServerSocket(); // 绑定监听端口号 serverSocket.bind(new InetSocketAddress(8080)); while (true) { System.out.println("开始等待接收数据中..."); Socket accept = serverSocket.accept(); int read = 0; read = accept.getInputStream().read(bytes); String result = new String(bytes); System.out.println("接收到数据:" + result); } } catch (IOException e) { e.printStackTrace(); } }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

在这里插入图片描述

再启动一个Socket客户端,先不进行输入。

package com.nobody;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/7/4
 */
public class ClientTcpSocket { public static void main(String[] args) { Socket socket = new Socket(); try { // 与服务端建立连接 SocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 8080); socket.connect(socketAddress); while (true) { Scanner scanner = new Scanner(System.in); socket.getOutputStream().write(scanner.next().getBytes()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }

}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

再启动另外一个Socket客户端02,输入client02

package com.nobody;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/7/4
 */
public class ClientTcpSocket02 { public static void main(String[] args) { Socket socket = new Socket(); try { // 与服务端建立连接 SocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 8080); socket.connect(socketAddress); while (true) { Scanner scanner = new Scanner(System.in); socket.getOutputStream().write(scanner.next().getBytes()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }

}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在这里插入图片描述
此时可以看到服务端没有接收到数据,因为Socket客户端01先连接,但是还未输入数据,所以服务端一直等待客户端01的输入,导致客户端02阻塞。

如果我们这时在客户端01输入client01,服务端控制台显示如下,先输出客户端01的数据,完成后才能输出客户端02的数据。
在这里插入图片描述
当然,如果不想后连接的客户端不阻塞,可以使用多线程实现伪异步IO,只需将服务端代码修改为如下:

public static void main(String[] args) { try { // 创建ServerSocket final ServerSocket serverSocket = new ServerSocket(); // 绑定监听端口号 serverSocket.bind(new InetSocketAddress(8080)); while (true) { System.out.println("开始等待接收数据中..."); Socket accept = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { int read = 0; try { read = accept.getInputStream().read(bytes); } catch (IOException e) { e.printStackTrace(); } String result = new String(bytes); System.out.println("接收到数据:" + result); } }).start(); } } catch (IOException e) { e.printStackTrace(); }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

当然上面代码有个缺点是创建的线程会频繁创建和销毁,频繁进行CPU调度,并且也消耗内存资源,可使用线程池机制优化。

NIO非阻塞式Socket过程:
前面两个客户端代码不变,服务端代码如下:

package com.nobody.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/7/4
 */
public class NioServer { private Selector selector; public void iniServer() { try { // 创建管道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 设置管道为非阻塞 serverSocketChannel.configureBlocking(false); // 将管道绑定到8080端口 serverSocketChannel.bind(new InetSocketAddress(8080)); // 创建一个选择器 this.selector = Selector.open(); // 将管道注册到选择器上,注册为SelectionKey.OP_ACCEPT事件, // 当事件到达后,selector.select()会返回,否则改方法会一直阻塞。 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void listen() throws IOException { System.out.println("服务端启动成功..."); // 轮询访问Selector while (true) { // 当事件到达后,selector.select()会返回,否则改方法会一直阻塞。 int select = selector.select(10); // 没有发送消息,跳过 if (0 == select) { continue; } // selector中选中的注册事件 Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 删除已选中的key,避免重复处理 iterator.remove(); if (key.isAcceptable()) { // 客户端连接事件 ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获得与客户端连接的管道 SocketChannel socketChannel = server.accept(); // 设置管道为非阻塞 socketChannel.configureBlocking(false); // 与客户端连接后,为了能接收到客户端的消息,为管道设置可读权限 socketChannel.register(this.selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 可读事件 // 创建读取数据的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(512); SocketChannel channel = (SocketChannel) key.channel(); channel.read(byteBuffer); byte[] bytes = byteBuffer.array(); String msg = new String(bytes).trim(); System.out.println("服务端收到消息:" + msg); ByteBuffer outByteBuffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8)); // 回应消息给客户端 channel.write(outByteBuffer); } } } } public static void main(String[] args) throws IOException { NioServer nioServer = new NioServer(); nioServer.iniServer(); nioServer.listen(); }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

启动服务端,然后再启动两个客户端,两个客户端都不会阻塞。
在这里插入图片描述

文章来源: javalib.blog.csdn.net,作者:陈皮的JavaLib,版权归原作者所有,如需转载,请联系作者。

原文链接:javalib.blog.csdn.net/article/details/107117828

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。