深入java核心_Java核心(五)深入理解BIO、NIO、AIO

导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别;BIO、NIO、AIO 的区别;理解和实现 NIO 操作 Socket 时的多路复用;同时掌握 IO 最底层最核心的操作技巧。

BIO、NIO、AIO 的区别是什么?

同/异步、阻/非阻塞的区别是什么?

文件读写最优雅的实现方式是什么?

NIO 如何实现多路复用功能?

带着以上这几个问题,让我们一起进入IO的世界吧。

在开始之前,我们先来思考一个问题:我们经常所说的“IO”的全称到底是什么?

可能很多人看到这个问题和我一样一脸懵逼,IO的全称其实是:Input/Output的缩写。

一、IO 介绍

我们通常所说的 BIO 是相对于 NIO 来说的,BIO 也就是 Java 开始之初推出的 IO 操作模块,BIO 是 BlockingIO 的缩写,顾名思义就是阻塞 IO 的意思。

1.1 BIO、NIO、AIO的区别

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。

NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

1.2 全面认识 IO

传统的 IO 大致可以分为4种类型:

InputStream、OutputStream 基于字节操作的 IO

Writer、Reader 基于字符操作的 IO

File 基于磁盘操作的 IO

Socket 基于网络操作的 IO

java.net 下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。

java.io 下的类和接口很多,但大体都是 InputStream、OutputStream、Writer、Reader 的子集,所有掌握这4个类和File的使用,是用好 IO 的关键。

1.3 IO 使用

接下来看 InputStream、OutputStream、Writer、Reader 的继承关系图和使用示例。

1.3.1 InputStream 使用

继承关系图和类方法,如下图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

InputStream 使用示例:

InputStream inputStream = new FileInputStream("D:\\log.txt");

byte[] bytes = new byte[inputStream.available()];

inputStream.read(bytes);

String str = new String(bytes, "utf-8");

System.out.println(str);

inputStream.close();

1.3.2 OutputStream 使用

继承关系图和类方法,如下图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

OutputStream 使用示例:

OutputStream outputStream = new FileOutputStream("D:\\log.txt",true); // 参数二,表示是否追加,true=追加

outputStream.write("你好,老王".getBytes("utf-8"));

outputStream.close();

1.3.3 Writer 使用

Writer 继承关系图和类方法,如下图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

Writer 使用示例:

Writer writer = new FileWriter("D:\\log.txt",true); // 参数二,是否追加文件,true=追加

writer.append("老王,你好");

writer.close();

1.3.4 Reader 使用

Reader 继承关系图和类方法,如下图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

Reader 使用示例:

Reader reader = new FileReader(filePath);

BufferedReader bufferedReader = new BufferedReader(reader);

StringBuffer bf = new StringBuffer();

String str;

while ((str = bufferedReader.readLine()) != null) {

bf.append(str + "\n");

}

bufferedReader.close();

reader.close();

System.out.println(bf.toString());

二、同步、异步、阻塞、非阻塞

上面说了很多关于同步、异步、阻塞和非阻塞的概念,接下来就具体聊一下它们4个的含义,以及组合之后形成的性能分析。

2.1 同步与异步

同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

2.2 阻塞与非阻塞

阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

2.3 同/异、阻/非堵塞 组合

同/异、阻/非堵塞的组合,有四种类型,如下表:

组合方式性能分析

同步阻塞最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。

同步非阻塞提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上。

异步阻塞这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。

异步非阻塞这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。

# 三、优雅的文件读写

Java 7 之前文件的读取是这样的:

// 添加文件

FileWriter fileWriter = new FileWriter(filePath, true);

fileWriter.write(Content);

fileWriter.close();

// 读取文件

FileReader fileReader = new FileReader(filePath);

BufferedReader bufferedReader = new BufferedReader(fileReader);

StringBuffer bf = new StringBuffer();

String str;

while ((str = bufferedReader.readLine()) != null) {

bf.append(str + "\n");

}

bufferedReader.close();

fileReader.close();

System.out.println(bf.toString());

Java 7 引入了Files(java.nio包下)的,大大简化了文件的读写,如下:

// 写入文件(追加方式:StandardOpenOption.APPEND)

Files.write(Paths.get(filePath), Content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);

// 读取文件

byte[] data = Files.readAllBytes(Paths.get(filePath));

System.out.println(new String(data, StandardCharsets.UTF_8));

读写文件都是一行代码搞定,没错这就是最优雅的文件操作。

Files 下还有很多有用的方法,比如创建多层文件夹,写法上也简单了:

// 创建多(单)层目录(如果不存在创建,存在不会报错)

new File("D://a//b").mkdirs();

四、Socket 和 NIO 的多路复用

本节带你实现最基础的 Socket 的同时,同时会实现 NIO 多路复用,还有 AIO 中 Socket 的实现。

4.1 传统的 Socket 实现

接下来我们将会实现一个简单的 Socket,服务器端只发给客户端信息,再由客户端打印出来的例子,代码如下:

int port = 4343; //端口号

// Socket 服务器端(简单的发送信息)

Thread sThread = new Thread(new Runnable() {

@Override

public void run() {

try {

ServerSocket serverSocket = new ServerSocket(port);

while (true) {

// 等待连接

Socket socket = serverSocket.accept();

Thread sHandlerThread = new Thread(new Runnable() {

@Override

public void run() {

try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) {

printWriter.println("hello world!");

printWriter.flush();

} catch (IOException e) {

e.printStackTrace();

}

}

});

sHandlerThread.start();

}

} catch (IOException e) {

e.printStackTrace();

}

}

});

sThread.start();

// Socket 客户端(接收信息并打印)

try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));

bufferedReader.lines().forEach(s -> System.out.println("客户端:" + s));

} catch (UnknownHostException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

调用 accept 方法,阻塞等待客户端连接;

利用 Socket 模拟了一个简单的客户端,只进行连接、读取和打印;

在 Java 中,线程的实现是比较重量级的,所以线程的启动或者销毁是很消耗服务器的资源的,即使使用线程池来实现,使用上述传统的 Socket 方式,当连接数极具上升也会带来性能瓶颈,原因是线程的上线文切换开销会在高并发的时候体现的很明显,并且以上操作方式还是同步阻塞式的编程,性能问题在高并发的时候就会体现的尤为明显。

以上的流程,如下图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

4.2 NIO 多路复用

介于以上高并发的问题,NIO 的多路复用功能就显得意义非凡了。

NIO 是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。

// NIO 多路复用

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4,

60L, TimeUnit.SECONDS, new LinkedBlockingQueue());

threadPool.execute(new Runnable() {

@Override

public void run() {

try (Selector selector = Selector.open();

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {

serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));

serverSocketChannel.configureBlocking(false);

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {

selector.select(); // 阻塞等待就绪的Channel

Set selectionKeys = selector.selectedKeys();

Iterator iterator = selectionKeys.iterator();

while (iterator.hasNext()) {

SelectionKey key = iterator.next();

try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {

channel.write(Charset.defaultCharset().encode("你好,世界"));

}

iterator.remove();

}

}

} catch (IOException e) {

e.printStackTrace();

}

}

});

// Socket 客户端(接收信息并打印)

try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));

bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));

} catch (IOException e) {

e.printStackTrace();

}

首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色;

然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求;

为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常;

Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒;

下面的图,可以有效的说明 NIO 复用的流程:

AAffA0nNPuCLAAAAAElFTkSuQmCC

就这样 NIO 的多路复用就大大提升了服务器端响应高并发的能力。

4.3 AIO 版 Socket 实现

Java 1.7 提供了 AIO 实现的 Socket 是这样的,如下代码:

// AIO线程复用版

Thread sThread = new Thread(new Runnable() {

@Override

public void run() {

AsynchronousChannelGroup group = null;

try {

group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));

server.accept(null, new CompletionHandler() {

@Override

public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {

server.accept(null, this); // 接收下一个请求

try {

Future f = result.write(Charset.defaultCharset().encode("你好,世界"));

f.get();

System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

result.close();

} catch (InterruptedException | ExecutionException | IOException e) {

e.printStackTrace();

}

}

@Override

public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {

}

});

group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

} catch (IOException | InterruptedException e) {

e.printStackTrace();

}

}

});

sThread.start();

// Socket 客户端

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();

Future future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));

future.get();

ByteBuffer buffer = ByteBuffer.allocate(100);

client.read(buffer, null, new CompletionHandler() {

@Override

public void completed(Integer result, Void attachment) {

System.out.println("客户端打印:" + new String(buffer.array()));

}

@Override

public void failed(Throwable exc, Void attachment) {

exc.printStackTrace();

try {

client.close();

} catch (IOException e) {

e.printStackTrace();

}

}

});

Thread.sleep(10 * 1000);

五、总结

以上基本就是 IO 从 1.0 到目前版本(本文的版本)JDK 8 的核心使用操作了,可以看出来 IO 作为比较常用的基础功能,发展变化的改动也很大,而且使用起来也越来越简单了,IO 的操作也是比较好理解的,一个输入一个输出,掌握好了输入输出也就掌握好了 IO,Socket 作为网络交互的集成功能,显然 NIO 的多路复用,给 Socket 带来了更多的活力和选择,用户可以根据自己的实际场景选择相应的代码策略。

六、参考文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/533364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java arraylist底层实现原理_ArrayList和LinkedList底层原理

ArrayList和LinkedList都是List的实现类,是在日常开发中经常被使用到的两个集合,我们来结合源码看下两个集合的不同之处。先来看下ArrayList的源码:// 默认的初始化大小private static final int DEFAULT_CAPACITY 10;ArrayList的底层数数组…

java 3_Java 3 (Java的数据类型)

Java的数据类型主要内容:1Java数据类型的分类2.8种基本数据类型3.理解引用类型的特点一、什么是数据类型?计算机语言将数据按性质进行分类,每一类称为一种数据类型;数据类型定义了数据的性质、取值范围、存储方式、对数据所能进行…

java replace stringbuilder_java.lang.StringBuilder.replace()方法实例

全屏java.lang.StringBuilder.replace()方法按照这个顺序,在指定的字符串的子字符串替换字符。子串开始在指定start的 索引,并延伸到该字符 end - 1,或如果序列的末端不存在这样的字符。声明以下是java.lang.StringBuilder.replace()方法的声…

中小学课java_java毕业设计_springboot框架的中小学排课与实现

这是一个基于java的毕业设计项目,毕设课题为springboot框架的中小学排课与实现, 是一个采用b/s结构的javaweb项目, 开发工具eclipsei/eclipse, 项目框架jspspringbootmybatis, 中小学排课与实现采用mysql进行数据存储, 并基于mybatis进行了orm实体关系映射, 该中小学排课与实现…

制作自己的 Docker 容器

软件开发最大的麻烦事之一,就是环境配置。用户必须保证操作系统的设置,各种库和组件的安装,只有它们都正确,软件才能运行。docker从根本上解决问题,软件安装的时候,把原始环境一模一样地复制过来。 以 koa-…

matlab差分算子的灰度图像边缘检测,常用图像边缘检测方法及MATLAB研究

论文2 1年 2月 I 01 5日现代电子技术M o e n El c r i sT e h qu d r e ton c c ni e第3 4卷第 4期Fe .2 11 b 0 Vo1 3 . . 4 NO 4常用图像边缘检测方法及 Malb研究 t a韦炜(安文理学院,陕西西安西 706 ) 1 0 5({№吨~一~一一一三一一垂”. ; _堇;~~ _一…

php %3c%3c%3cxml 报错,代码审计| APPCMS SQL-XSS-CSRF-SHELL

0x01 背景由若水师傅提供的一个素材,想要复现CNVD上披露的一个APPCMS的漏洞,由CNVD上的描述可以知道存在漏洞的地方是comment.php这个文件,然后就没有详细的漏洞信息了,所以就需要分析相应的源码文件找出存在漏洞的点。借这个素材…

php二进制保存到本地,C# 将二进制字符串保存到本地

C# 将二进制字符串保存到本地#region 将文件保存到本地/// /// 将文件保存到本地/// /// 文件的二进制数据字符串/// 文件名称,必须带后缀private void SaveFile(string psContent, string psFileName){byte[] accessory Convert.FromBase64String(psContent);//Sy…

suse 安装oracle11,Suse11安装Oracle11gR2

注:以下采用终端XmanagerEnterprise 4中的Xshell连接1、安装前参数修改vi /etc/security/limits.conf --末尾添加如下oracle soft nproc 2047oracle hard nproc 16384oracle soft nofile 1024oracle hard nofile 65536vi /etc/sysctl.conf --末尾添加如…

oracle 超市管理系统,SuperManager 超市账单管理系统 JSP + Servlet + Oracle Jsp/ 240万源代码下载- www.pudn.com...

文件名称: SuperManager下载 收藏√ [5 4 3 2 1 ]开发工具: Java文件大小: 2144 KB上传时间: 2015-07-07下载次数: 0详细说明:超市账单管理系统JSP Servlet Oracle-超市账单管理系统JSP Servlet Oracle文件列表(点击判断是否您需要的文件,如果是…

linux命令行的操作符,如何在Linux命令行中进行基本的数学运算

原标题:如何在Linux命令行中进行基本的数学运算Linux bash或命令行允许您执行基本和复杂的算术和布尔运算。像expr,jot,bc和factor等命令可以帮助您找到复杂问题的最优数学解决方案。在本文中,我们将描述这些命令并提供示例&#…

linux什么用户什么任务,linux任务里的1 和2是什么意思

输出学过代码的小伙伴应该知道STDIN、STDOUT、STDERR通常都是指定输出通道的,perl里又称之为句柄那么1代表的就是STDOUT、2代表的是STDERR、jimmy在视频中会翻译成1代表的是正确输出,2代表的是错误输出。其实严格上不能这样去固有化去理解每一个软件的定…

linux .desktop权限,如何在Ubuntu Xenial Xerus 16.04 Linux Desktop上以root用户身份登录

您可能已经注意到,默认情况下,Ubuntu Xenial Xerus 16.04 Linux Desktop不具备以root管理员用户身份登录的功能。每次尝试以root用户身份在终端上登录都会导致Login incorrect错误信息:。默认的Ubuntu Linux桌面行为的背后原因是,…

2048游戏c语言linux简易代码,C语言实现2048游戏代码

本文实例为大家分享了C语言实现2048游戏具体代码,供大家参考,具体内容如下效果图:使用文本界面的屏幕绘图库 ncurses.设计思路:在满足条件情况下消除方块允许在游戏主界面(16 宫格)中任意一格输出数据实现代码:#include #include #include #include #inc…

linux shell结构,linux——Shell的控制结构(附shell编写代码和运行结果)

针对shell的控制结构,也就是shell编程时所需要的三种控制流程,顺序/分支和循环。在bash中,顺序可由简单的输入输出命令组成;分支语句由if、case实现;循环语句用for、while和until来实现。一、if语句1、基本的if语句语句…

c语言 三个小球排排坐,关颖三个孩子排排坐 太萌啦

0关颖三个孩子排排坐 太萌啦2019-12-10 10:596月20日,关颖在微博上晒出三个孩子坐在垫子上的照片,配文:“Terrible two has officially started today. Happy happy birthday Phi Phi! 家庭乐趣其中一件事情 就是和小朋友不断的唱生日歌吹蜡烛…

大学生学C语言用什么笔记本电脑,有哪些适合大学生用的笔记本电脑

高考成绩公布之后又有一大波新大学生即将入学,笔记本电脑也将是大学生必不可少的一款电子数码产品,但是现在市面上电脑繁多,又有哪些比较不错的电脑适合新入学的大学生呢?惠普 HP Envy 13 (2019)现在,最适合学生的笔记…

android自带下拉阻尼动画,android 有阻尼下拉刷新列表的实现方法

本文将会介绍有阻尼下拉刷新列表的实现,先来看看效果预览:这是下拉状态:这是下拉松开手指后listView回滚到刷新状态时的样子:1. 如何调用虽然效果图看起来样子不太好看,主要是因为那个蓝色的背景对不对,没关…

android viewpager画廊,Android使用ViewPager实现画廊效果

按照国际惯例,先上效果图其实这跟普通的ViewPager原理都一样,需要改变的地方就是:1.增加滑进和滑出的动画效果2.缩小ViewPager的大小,给屏幕上留出上一张和下一张视图的空间布局文件:xmlns:android"http://schema…

excel 区间人数柱状图_Excel中,区间统计的3种技巧都不掌握,那就真的OUt了!

点击上方"Excel函数公式"免费订阅 Excel的最大功能在于数据的分析与处理,在数据分析和处理中,区间统计是非常广泛的,各位亲是怎么操作的呢?如果还不掌握,且看小编给大家带来的“区间统计”的3种应用技巧。一…