Java程序员从笨鸟到菜鸟(四十三)NIO 非阻塞实现高并发

2024-08-28 20:08

本文主要是介绍Java程序员从笨鸟到菜鸟(四十三)NIO 非阻塞实现高并发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、阻塞和非阻塞

阻塞:应用程序在获取网络数据的时候,如果网络传输很慢,就会一直等待直到传输完毕为止

非阻塞:应用程序可以直接获取已经准备就序好的数据,无需等待

二、BIO、NIO、AIO

BIO(同步阻塞式 IO):服务器实现模式为一个请求一个线程,客户端有连接请求是服务器就需要启动一个线程进行处理,如果这个连接不做任何事情就造成不必要的开销

NIO(同步非阻塞式IO):服务器实现模式一个请求一个线程,客户端发送连接请求都会注册到多路复用器上,多路复用轮询到连接有 IO 请求时才启动一个线程处理,

AIO(异步非阻塞式 IO):JDK 1.7 后升级为 NIO2.0,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理

同步时:应用程序会直接参与 IO 读写操作,并且应用程序会阻塞到某个方法上,直到数据准备就绪,或者采用轮询策略实时检查数据的就绪状态,如果就绪则获取数据

异步时:所有的 IO 读写操作交给操作系统,与应用程序没有直接关系,不需要关系 IO 读写,当操作系统完成了 IO 读写操作,会给我们应用程序发送通知,直接拿走数据即可

伪异步
如何解决同步阻塞 IO?使用 伪异步阻塞 IO(多线程)
多线程没有真正解决阻塞 IO 核心,频繁创建线程,也会占用 CPU 内存,使电脑性能下降,推荐使用线程池进行管理

IO 模型
这里写图片描述

三、解决阻塞

传统方式:使用线程池
这里写图片描述
在服务端启动时创建线程池,当监听到客户端连接时,就为客户端创建一个线程,并将该线程放入线程池中即可。这样在该客户断开连接时,该客户端的处理线程就会被归还到线程池中,以提高线程的池化管理,提高线程的使用效率

弊端:每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的IO操作上,没有有效利用CPU

NIO 非阻塞 IO
这里写图片描述
NIO 非阻塞 IO 机制是围绕Selector(选择器)Channel(通道)构建的,Channel 类表示服务器与客户端的一种通信机制,Selector 类是 Channel 的多路复用

Selector 监听器负责轮询客户端客户端的连接请求、读取和写入事件,这些事件的执行都不会被阻塞,为了提高执行效率, NIO 在读取和写入的数据中使用了缓存

内容传送门:https://blog.csdn.net/luliuliu1234/article/details/61914097
非常感谢作者

四、NIO 非阻塞代码

NIO 客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Scanner;/*** create by tan on 2018/7/11* 非阻塞式 io 客户端**/
public class NioClient {public static void main(String[] args) throws IOException {System.out.println("...客户端启动了...");// 1. 创建通道SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));// 2. 切换非阻塞socketChannel.configureBlocking(false);// 3. 指定缓冲区大小ByteBuffer allocate = ByteBuffer.allocate(1024);Scanner scanner = new Scanner(System.in);System.out.println("请输入内容:");while (scanner.hasNext()) {System.out.println("请输入内容:");String str = scanner.nextLine();// 向缓冲区写入数据allocate.put((new Date().toString() + "\n" + str).getBytes());// 准备从缓冲区读取已经写入的数据,准备缓冲器,以便信息可以由 write() 读取allocate.flip();// 向套接字通道写入数据socketChannel.write(allocate);// 不会改变缓冲区的数据,只是改变了缓冲器的索引值,方便重用缓冲区allocate.clear();}socketChannel.close();}
}

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;/*** create by tan on 2018/7/11* 非阻塞式 IO 服务端**/
public class NioServer {public static void main(String[] args) throws IOException {System.out.println("...服务端已经启动了...");// 1. 创建通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 切换非阻塞,channel 与 selector 一起使用时,必须是处于非阻塞模式下serverSocketChannel.configureBlocking(false);// 3. 绑定连接serverSocketChannel.bind(new InetSocketAddress(9999));// 4. 获取选择器Selector selector = Selector.open();// 5. 将通道注册到选择器,并且指定监听接收事件serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);// 6. 轮询式获取“已经准备就绪”的事件while (selector.select() > 0) {// 7. 获取当前选择器所在的注册的“选择键”Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {// 8. 获取准备就绪的事件SelectionKey selectionKey = iterator.next();// 9. 判断具体已准备就绪的事件if (selectionKey.isAcceptable()) {// 10. 若已经就绪,获取客户端连接SocketChannel socketChannel = serverSocketChannel.accept();// 11. 设置阻塞模式socketChannel.configureBlocking(false);// 12. 在服务器上注册通道socketChannel.register(selector,SelectionKey.OP_READ);} else if (selectionKey.isReadable()) {// 13. 获取当前选择器就绪状态的通道SocketChannel socketChannel = (SocketChannel)selectionKey.channel();// 14. 读取数据ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int len = 0;while ((len = socketChannel.read(byteBuffer)) > 0) {byteBuffer.flip();System.out.println(new String(byteBuffer.array(), 0 ,len));byteBuffer.clear();}}}iterator.remove();}serverSocketChannel.close();}
}

启动客户端和服务端进行通信
这里写图片描述

服务端操作过程
  1. 创建 ServerSocketChannel 实例,设置成非阻塞式,并绑定指定端口
  2. 创建 Selector 实例
  3. 将 ServerSocketChannel 注册到 Selector,并指定事件 OP_ACCEPT,,socket 通过 channel 和 selector 建立关联
  4. 如果没有准备好 socket,select 会一直阻塞一段时间并返回0
  5. 如果底层 socket 已准备好,selector 的 select 方法会返回 socket 的个数,而 selectedKeys 方法会返回 socket 对应的事件(connect、accept、read or write)
  6. 根据事件类型,处理不同的逻辑

在步骤 3 中,selector 只注册了 serverSocketChannel 的 OP_ACCEPT 事件

  1. 如果客户端 A 连接服务,执行 select 方法时,可以通过 ServerSocketChannel 获取客户端 A 的 socketChannel,并在 selector 上注册的 socketChannel 的 OP_READ 事件
  2. 如果客户端 A 发送数据,会触发 read 事件,这样下次轮询 select 时,就能通过 socketChannel 读取数据,同时在 selector 上注册该 socketChannel 的 OP_WRITE 事件,实现客户端往服务器写数据
selector

selector 的创建:调用 open() 方法

Selector selector = Selector.open()
  • isOpen() —- 判断 Selector 是否处于打开状态,创建的时候就打开了
  • close() —- 当调用了 Selector 对象的 close 方法,就进入关闭状态 ,并使注册在 Selector 上的所有 SelectionKey 实例无效,通道本身并不会关闭

向 Selector 注册通道
为了将 Channel 和 Selector 配合使用,必须将 channel 注册到 Selector 上

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);

与 Selector 一起使用时,channel 必须处于非阻塞模式,也就是状态标为 false
也就意味着 FileChannel 不能与 Selector 一起使用
resgiter() 第二个参数可以监听四种不同类型的事件

  • SelectionKey.OP_CONNECT 连接就绪
  • SelectionKey.OP_ACCEPT 接收就绪
  • SeletionKey.OP_READ 读就绪
  • SelectionKey.OP_WRITE 写就绪

register() 返回值 —— SelectionKey
一个 Selector 对象包含 3 种类型的 SelectionKey 集合:

  • all-keys 集合 —— 当前所有向 Selector 注册的 SelectionKey 集合,Selector 的 keys() 方法返回该集合
  • selected-keys 集合 —— 相关事件已经被 Selectors 捕获的 SelectionKey 集合,Selector 的 seelctedKeys() 方法返回该集合
  • cancelled-keys 集合 —— 已经被取消的 SelectionKey 的集合,Selector 没有提供访问该集合的方法

Selector 的 wakeUp() 方法
当某个线程调用 select() 方法阻塞了,即便没有通道已经就绪,也有办法让其从 select() 方法返回,只需让其他线程在第一个线程调用 select() 方法上的对象调用 Selector 的 weakup() 方法s,阻塞在 select() 方法上的线程会立马返回

Selector 部分内容传送门:https://blog.csdn.net/robinjwong/article/details/41792623 非常感谢作者

代码块传送门:https://blog.csdn.net/yswKnight/article/details/79347833 非常感谢作者

这篇关于Java程序员从笨鸟到菜鸟(四十三)NIO 非阻塞实现高并发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1115760

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

C#实现一键批量合并PDF文档

《C#实现一键批量合并PDF文档》这篇文章主要为大家详细介绍了如何使用C#实现一键批量合并PDF文档功能,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言效果展示功能实现1、添加文件2、文件分组(书签)3、定义页码范围4、自定义显示5、定义页面尺寸6、PDF批量合并7、其他方法

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2