java高并发实战(八)——BIO、NIO和AIO

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

一、什么是NIO?

NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标准。是一种多路复用;它是在Java 1.4中被纳入到JDK中的,并具有以下特性:

    – NIO是基于块(Block)的,它以块为基本单位处理数据而传统IO是以字节流的形式,因此NIO性能好一些。

    – 为所有的原始类型提供(Buffer)缓存支持– 增加通道(Channel)对象作为新的原始 I/O 抽象

    – 支持锁(文件锁:有些程序运行时产生.log文件,他就是文件锁,表示当前线程在占用文件锁,其他线程要使用该锁时,需要等待阻塞,当使用完锁时后会删除该文件,也就是拿文件系统来实现锁)和内存映射文件的文件访问接口(一个文件在硬盘磁盘中的,把文件映射到内存,把文件读到内存中,比传统把文件中数据一个个读进去快的多)

    – 提供了基于Selector的异步网络I/O

详细请看https://blog.csdn.net/gududedabai/article/details/80783994

二、Buffer&Channel


所有的IO/NIO操作(读写)都要经过Buffer,他是NIO核心部分。通道就是一个抽象,他的一端可能是一个文件或socket等,往里写数据。也就是说通过对buffer读写来对实际的NIO目标来读取实现等。

缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区:

使用缓冲区有这么两个好处:

1、减少实际的物理读写次数

2、缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数

举个简单的例子,比如A地有1w块砖要搬到B地

由于没有工具(缓冲区),我们一次只能搬一本,那么就要搬1w次(实际读写次数)

如果A,B两地距离很远的话(IO性能消耗),那么性能消耗将会很大

但是要是此时我们有辆大卡车(缓冲区),一次可运5000本,那么2次就够了

相比之前,性能肯定是大大提高了。

Buffer类详细用法:https://www.cnblogs.com/jiduoduo/p/6397454.html

下面是buffer的实现:

1、buffer


buffer简单的使用:

FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//ByteBuffer.allocate()方法分配了一段内存空间,作为缓存
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();//读写转换

(1)通过NIO复制文件

public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //读文件通道
FileChannel writeChannel = fos.getChannel(); //写文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //读入数据缓存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //读入数据
if (len == -1) {
break;
//读取完毕
}
buffer.flip();
writeChannel.write(buffer);
//写入文件
}
readChannel.close();
writeChannel.close();
}

(2)Buffer中有3个重要的参数:位置(position)、容量(capactiy)和上限(limit)



通过案例了解三个参数的意义:

ByteBuffer b=ByteBuffer.allocate(15); //15个字节大小的缓冲区
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<10;i++){ //存入10个字节数据
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
for(int i=0;i<5;i++){
System.out.print(b.get());
}
System.out.println();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip();
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());


flip转换主要两步操作:把postition置零;把limit前移到10,就是迁移到刚才postition的位置(好处是,下一次操作自然是从postiton的0开始,操作到那一步是有意义的呢,就是limit的位置,换句话说,从postition到limit之间数据是我上一次写入的的数据而且是有意义的数据,从limit到capacity并不是我上次写入的数据,可能是没有意义的数据,因此在此时读缓冲区,就可以读postition到limit之间的数据就可以了,因此flip通常用于读写转换,在filp之前用于写,在filp之后通常用于读,读使因为位置已经置零了,而且此时数据到limit是有意义的数据。)。

该操作会重置position,通常,将buffer从写模式转换为读模式时需要执行此方法flip()操作不仅重置了当前的position为0,还将limit设置到当前position的位置。


 public final Buffer rewind()

    – 将position置零,并清除标志位(mark)就是把之前读的数据在扫一遍,limit位置是不变的,因此数据是基本一样的

 public final Buffer clear()

    – 将position置零,同时将limit设置为capacity的大小,并清除了标志mark

 public final Buffer flip()

    – 先将limit设置到position所在位置,然后将position置零,并清除标志位mark– 通常在读写转换时使用

(3)文件映射到内存

RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel();
//将文件映射到内存中
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();

三、网络编程

多线程网络服务器的一般结构


下面实现上面简单案例:

1.简单案例 EchoServer

 ServerSocket echoServer = null;
 Socket clientSocket = null;
 try {
 echoServer = new ServerSocket(8000);
 } catch (IOException e) {
 System.out.println(e);
 }
 while (true) {
 try {
 clientSocket = echoServer.accept();
 System.out.println(clientSocket.getRemoteSocketAddress() + " connect!");
 tp.execute(new HandleMsg(clientSocket));
 } catch (IOException e) {
 System.out.println(e);
 }
 }
 }
static class HandleMsg implements Runnable{
省略部分信息
 public void run(){
 try {
 is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
 os = new PrintWriter(clientSocket.getOutputStream(), true);
 // 从InputStream当中读取客户端所发送的数据
 String inputLine = null;
 long b=System.currentTimeMillis();//统计时间戳
 while ((inputLine = is.readLine()) != null) {
 os.println(inputLine);
 }
 long e=System.currentTimeMillis();
 System.out.println("spend:"+(e-b)+"ms");
 } catch (IOException e) {
 e.printStackTrace();
 }finally{
关闭资源
}
 }

2.EchoServer的客户端

public static void main(String[] args) throws IOException {
 Socket client = null;
 PrintWriter writer = null;
 BufferedReader reader = null;
 try {
 client = new Socket();//创建socket
 client.connect(new InetSocketAddress("localhost", 8000));
 writer = new PrintWriter(client.getOutputStream(), true);
 writer.println("Hello!");
 writer.flush();
 reader = new BufferedReader(new InputStreamReader(client.getInputStream()));//读数据,服务器返回的数据从socket中
 System.out.println("from server: " + reader.readLine());
 } catch
 } finally {
 //省略资源关闭
 }
}

 问题:

    – 为每一个客户端使用一个线程,如果客户端出现延时等异常,线程可能会被占用很长时间。因为数据的准备和读取都在这个线程中。

    – 此时,如果客户端数量众多,可能会消耗大量的系统资源

 解决

    – 非阻塞的NIO

    – 数据准备好了在工作

3.网络编程-模拟低效的客户端

private static ExecutorService tp=Executors.newCachedThreadPool();
private static final int sleep_time=1000*1000*1000;
public static class EchoClient implements Runnable{
public void run(){
 try {
 client = new Socket();
 client.connect(new InetSocketAddress("localhost", 8000));//开辟若干个线程
 writer = new PrintWriter(client.getOutputStream(), true);
 writer.print("H");
 LockSupport.parkNanos(sleep_time);
 writer.print("e");
 LockSupport.parkNanos(sleep_time);
 writer.print("l");
 LockSupport.parkNanos(sleep_time);
 writer.print("l");
 LockSupport.parkNanos(sleep_time);
 writer.print("o");
 LockSupport.parkNanos(sleep_time);
 writer.print("!");
 LockSupport.parkNanos(sleep_time);
 writer.println();
 writer.flush();

服务器输出如下:

spend:6000ms
spend:6000ms
spend:6000ms
spend:6001ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6002ms
spend:6003ms
spend:6003ms

4.网络编程-NIO

 把数据准备好了再通知我

    Channel有点类似于流,一个Channel可以和文件或者网络Socket对应


一个线程(也就是selector)可以轮询多个channel,每一个channl下面对应一个socket。

select()与selectNow()用来看那个channel(客户顿)准备好数据,selector用select()当数据没有转备好他会被阻塞.

selectNow()与select()一样,但区别是他是无阻赛的,没有转备好则返回0;

 总结:

    – NIO会将数据准备好后,再交由应用进行处理(因此数据没有转备好他是不会写入应用中间的,所以节省资源),数据的读取过程依然在应用线程中完成

    – 节省数据准备时间(因为Selector可以复用)

5.网络编程 AIO(主要采用异步回调方式,他是异步的IO)

 读完了再通知我(就会既不需要读,也不会需要写,就等待就行了)

 不会加快IO,只是在读完后进行通知(什么时候拿到通知什么时候处理,在没有拿到通知去做其他事情,节省时间)

 使用回调函数,进行业务处理

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));

使用server上的accept方法

public abstract void accept(A attachment, CompletionHandlerhandler); 


read是从secket中读数据,因为是异步,所以会立即返回个future.,因此不会在等待读完才返回,不然不叫异步,异步是直接返回的。

write也是类似,写后会立即返回个future.

server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
 final ByteBuffer buffer = ByteBuffer.allocate(1024);
 public void completed(AsynchronousSocketChannel result, Object attachment) {
 System.out.println(Thread.currentThread().getName());
 Future<Integer> writeResult=null;
 try {
 buffer.clear();
 result.read(buffer).get(100, TimeUnit.SECONDS);//这里把异步有变成了类似同步与等待因为这里做了个阻塞。在这里也可以再来个回调函数去做一些处理
 buffer.flip();
 writeResult=result.write(buffer);
 } catch (InterruptedException | ExecutionException e) {
 e.printStackTrace();
 } catch (TimeoutException e) {
 e.printStackTrace();
 } finally {
 try {
 server.accept(null, this);//用于在做下一个请求操作,不想做完就程序就不在运行了
 writeResult.get();
 result.close();
 } catch (Exception e) {
 System.out.println(e.toString());
 }
 }
 }
 @Override
 public void failed(Throwable exc, Object attachment) {
 System.out.println("failed: " + exc);
 }
});

6、BIO、NIO与AIO对比:

1.同步

例:买饭:自己亲自去饭馆买饭,这就是同步(自己处理IO读写)

2.异步

例:买饭:叫外卖送到家,这就是异步(IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(饭名和地址),OS需要支持异步IO操作API)

3.阻塞

例:办理业务:一直排队等待(调用会一直阻塞到读写完成才返回)

4.非阻塞

例:办理业务:抽号后就可以做其他事,如果你等不急,可以去问工作人员到你了没,如果没到你就不能办理业务。(如果不能读写,调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

BIO是同步阻塞的

伪异步IO:通过线程池机制优化了BIO模型

NIO是同步非阻塞的

AIO是异步非阻塞的

由于NIO的读写过程依然在应用线程里完成,所以对于那些读写过程时间长的,NIO就不太适合。

而AIO的读写过程完成后才被通知,所以AIO能够胜任那些重量级,读写过程长的任务

BIO是服务器实现模式基于原始的IO(输入输出流)Socket就可以编写一个最基本的BIO服务器(用于通信,走TCP协议;什么是TCPhttps://www.cnblogs.com/buxiangxin/p/8336022.html;扩展TCP长连接与短连接https://www.cnblogs.com/songjy2116/p/7750039.html);一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,在JDK1.4出来之前使用的BIO

Socket编程就是BIO,一个socket连接一个处理线程(这个线程负责这个Socket连接的一系列数据传输操作)。阻塞的原因在于:操作系统允许的线程数量是有限的,多个socket申请与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接就会阻塞等待或被拒绝。

BIO图解:


伪异步IO

通过线程池机制优化了BIO模型

伪异步IO图解:


NIO模型

同步非阻塞

服务器实现模式为一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

NIO图解:

AIO模型

异步非阻塞

服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS(操作系统)先完成了再通知服务器应用去启动线程进行处理,

注:AIO又称为NIO2.0,在JDK7才开始支持。

AIO流程图 

BIO、伪异步、NIO和AIO模型的比较

BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。

BIO是一个连接一个线程。

NIO是一个请求一个线程。

AIO是一个有效请求一个线程。

Java对BIO、NIO、AIO的支持:

  • Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

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

BIO、NIO、AIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。

在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式:详细介绍:https://www.cnblogs.com/daoluanxiaozi/p/3274925.html,其中Reactor模式用于同步I/O,Proactor运用于异步I/O操作。

在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

 一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO

同步阻塞IO在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!

同步非阻塞IO:在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。

异步阻塞IO此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!

 异步非阻塞IO:在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。   


参考:https://blog.csdn.net/baiye_xing/article/details/73123753

https://blog.csdn.net/skiof007/article/details/52873421

https://www.cnblogs.com/ygj0930/p/6543960.html


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡之路无尽路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值