当前位置: 首页 > 科技观察

架构师的知识储备——深入理解BIO、NIO、AIO

时间:2023-03-17 18:20:26 科技观察

这篇文章,你将得到:同步/异步+阻塞/非阻塞的性能差异;BIO、NIO、AIO的区别;操作SocketMultiplexing时对NIO的理解和实现;同时掌握IO底层核心操作技能。BIO、NIO和AIO之间有什么区别?同步/异步和阻塞/非阻塞有什么区别?文件读写最优雅的实现方式是什么?NIO是如何实现多路复用的?带着以上的疑问,让我们一起走进IO的世界。在开始之前,我们先想一个问题:我们常说的“IO”全称是什么?可能很多人看到这个问题的时候和我一样一头雾水。IO的全称其实是:Input/Output的缩写。1.IO介绍我们通常所说的BIO是相对于NIO而言的。BIO是Java一开始就推出的IO操作模块。BIO是BlockingIO的缩写,顾名思义就是阻塞IO。1.1BIO、NIO、AIO的区别BIO是传统的java.io封装,基于流模型实现,交互方式为同步和阻塞。线程在操作完成之前一直处于阻塞状态,它们之间的调用具有可靠的线性顺序。它的优点是代码比较简单直观;缺点是IO的效率和扩展性很低,容易成为应用性能的瓶颈。NIO是Java1.4引入的java.nio包。它提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用、同步非阻塞IO程序,同时提供更接近操作系统底层高性能的数据操作方式。AIO是Java1.7之后引入的一个包。它是NIO的升级版。它提供了一种异步、非阻塞的IO操作方式,所以人们称之为AIO(AsynchronousIO)。异步IO是基于事件和回调机制实现的,也就是应用程序操作完之后,会直接返回,不会阻塞在那里。当后台处理完成后,操作系统会通知相应的线程进行后续操作。1.2全面理解IO传统IO大致可以分为四种类型:InputStream,OutputStreamIOWriterandReader基于字节操作的IOFile基于字符操作的IOSocket基于磁盘操作的IOSocketjava.net下提供的Scoket很多时候人们也把它归类为同步阻塞IO,因为网络通信也是IO行为。java.io下有很多类和接口,但大部分都是InputStream、OutputStream、Writer、Reader的子集。掌握这四个类和File的使用是用好IO的关键。1.3IO使用接下来我们看一下InputStream、OutputStream、Writer、Reader的继承图和使用示例。1.3.1InputStream使用继承图和类方法,如下图:InputStream使用示例:InputStreaminputStream=newFileInputStream("D:\\log.txt");byte[]bytes=newbyte[inputStream.available()];输入流。read(bytes);Stringstr=newString(bytes,"utf-8");System.out.println(str);inputStream.close();1.3.2OutputStream使用继承图和类方法,如图下图:OutputStream使用实例:OutputStreamoutputStream=newFileOutputStream("D:\\log.txt",true);//参数二,表示是否追加,true=appendoutputStream.write("Hello,Pharaoh".getBytes("utf-8"));outputStream.close();1.3.3Writer使用Writer继承关系图和类方法,如下图:Writer使用示例:Writerwriter=newFileWriter("D:\\log.txt",true);//参考编号二,是否追加文件,true=appendwriter.append("你好,老王");writer.close();1.3.4Reader使用Reader继承关系图和类方法,如下图所示:Line())!=null){bf.append(str+"\n");}bufferedReader.close();reader.close();System.out.println(bf.toString());二、同步、异步、阻塞、非阻塞关于同步、异步、阻塞、非阻塞的概念上面已经讲了很多。下面详细说说他们四个的含义,结合后形成的性能分析2.1同步和异步同步是指当一个任务的完成需要依赖另一个任务时,依赖的任务只有在完成后才算完成等待依赖任务完成,这是一个可靠的任务序列。要么成功就是成功,失败就是失败,两个任务的状态可以保持一致。异步不需要等待依赖任务完成,它只是通知依赖任务完成什么工作,依赖任务立即执行,只要整个任务自己完成即可。至于被依赖的任务最终是否真正完成,无法确定依赖于它的任务,因此是一个不可靠的任务序列。我们可以用电话和短信来很好地类比同步和异步操作。2.2阻塞和非阻塞阻塞和非阻塞主要是在CPU消耗上。阻塞意味着CPU停止并等待一个缓慢的操作完成,然后CPU才能完成其他事情。非阻塞是指CPU在执行慢操作的同时做其他事情,当慢操作完成后,CPU再完成后面的操作。非阻塞方式虽然看起来可以明显提高CPU的利用率,但是也带来了另一个后果,就是系统的线程切换增加了。增加的CPU使用时间是否可以补偿系统的切换成本需要仔细评估。2.3相同/不同,阻塞/非阻塞组合相同/不同,阻塞/非阻塞组合有四种,如下表所示:3.优雅的文件读写Java7之前的文件读取就像this://添加FileWriterfileWriter=newFileWriter(filePath,true);fileWriter.write(Content);fileWriter.close();//读取文件FileReaderfileReader=newFileReader(filePath);BufferedReaderbufferedReader=newBufferedReader(fileReader);StringBufferbf=newStringBuffer();字符串;while((str=bufferedReader.readLine())!=null){bf.append(str+"\n");}bufferedReader.close();fileReader.close();System.out.println(bf.toString());Java7引入了Files(在java.nio包下),大大简化了文件的读写,如下://写入文件(append方法:StandardOpenOption.APPEND)Files.write(Paths.get(filePath),Content.getBytes(StandardCharsets.UTF_8),StandardOpenOption.APPEND);//读取文件byte[]data=Files.readAllBytes(Paths.get(filePath));System.out.println(newString(data,StandardCharsets.UTF_8));一行代码读写文件,没错,这就是最优雅的文件操作。Files下有很多有用的方法,比如创建多层文件夹,写法也简单://创建多层(单)层目录(如果不存在,如果不存在则不会报错)它存在)newFile("D://a//b").mkdirs();4.Socket与NIO的复用本节将带大家实现最基本的Socket,同时实现NIO的复用,以及AIO中Socket的实现。4.1传统Socket的实现接下来我们来实现一个简单的Socket。服务器只向客户端发送信息,然后客户端打印出例子。代码如下:intport=4343;//端口号//Socket服务器(简单发送信息)ThreadsT??hread=newThread(newRunnable(){@Overridepublicvoidrun(){try{ServerSocketserverSocket=newServerSocket(port);while(true){//等待连接Socketsocket=serverSocket.accept();ThreadsHandlerThread=newThread(newRunnable(){@Overridepublicvoidrun(){try(PrintWriterprintWriter=newPrintWriter(socket.getOutputStream())){printWriter.println("helloworld!");printWriter.flush();}catch(IOExceptione){e.printStackTrace();}}});sHandlerThread.start();}}catch(IOExceptione){e.printStackTrace();}}});sThread。start();//Socket客户端(接收信息并打印)forEach(s->System.out.println("Client:"+s));}catch(UnknownHostExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}调用accept方法,阻塞等待客户端连接;使用Socket模拟一个简单的客户端,只有连接、读取和打印;在Java中,线程的实现是比较重量级的,所以线程的启动或者销毁非常重要消耗服务器资源,即使是使用线程池实现,使用上面提到的传统Socket方式,当连接数上升,就会带来性能瓶颈。显然,而且上面的运行方式还是同步阻塞编程,在并发高的时候性能问题会特别明显。复用功能非常有意义。NIO采用单线程轮询事件的机制,通过高效定位就绪的Channel来决定做什么。只有select阶段被阻塞,可以有效避免大量client连接时线程频繁切换带来的问题。扩展能力有了很大的提高。//NIO多路复用ThreadPoolExecutorthreadPool=newThreadPoolExecutor(4,4,60L,TimeUnit.SECONDS,newLinkedBlockingQueue());threadPool.execute(newRunnable(){@Overridepublicvoidrun(){try(Selectorselector=Selector.open();ServerSocketChannelserverSocketChannel=ServerSocketChannel.open();){serverSocketChannel.bind(newInetSocketAddress(InetAddress.getLocalHost(),port));serverSocketChannel.configureBlocking(false);serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);while(true){selector.select();//等待就绪的ChannelSetselectionKeys=selector.selectedKeys();Iteratoriterator=selectionKeys.iterator();while(iterator.hasNext()){SelectionKeykey=iterator.next();try(SocketChannelchannel=((ServerSocketChannel)key.channel()).accept()){channel.write(Charset.defaultCharset().encode("你好,世界"));}iterator.remove();}}}catch(IOExceptione){e.printStackTrace();}}});//Socket客户端(接收信息并打印)try(SocketcSocket=newSocket(InetAddress.getLocalHost(),port)){BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(cSocket.getInputStream()));bufferedReader.lines().forEach(s->System.out.println("NIOclient:"+s));}catch(IOExceptione){e.printStackTrace();}首先通过Selector.open()创建一个Selector,作用类似于调度器;然后,创建一个ServerSocketChannel,并通过指定SelectionKey.OP_ACCEPT向Selector注册,以告诉调度程序它正在处理新的连接请求;为什么我们需要显式配置非阻塞模式?这是因为在阻塞模式下,注册操作是不允许的,会抛出IllegalBlockingModeException;Selector阻塞在select操作中,当某个Channel有访问请求时,会被唤醒;下图可以有效的说明NIO多路复用的过程:这样,NIO多路复用大大提高了服务器对高并发的响应能力4.3AIO版本的Socket实现Java1.7提供了这样的Socket的AIO实现,代码如下:(Executors.newFixedThreadPool(4));AsynchronousServerSocketChannelserver=AsynchronousServerSocketChannel.open(group).bind(newInetSocketAddress(InetAddress.getLocalHost(),port));server.accept(null,newCompletionHandler(){@Overridepublicvoidcompleted(AsynchronousSocketChannelresult,AsynchronousServerSocketChannelattachment){server.accept(null,this);//接收下一个请求try{Futuref=result.write(Charset.defaultCharset().encode("Hello,world"));f.get();System.out.println("服务器发送时间:"+newSimpleDateFormat("yyyy-MM-ddHH:mm:ss").format(newDate()));result.close();}catch(InterruptedException|ExecutionException|IOExceptione){e.printStackTrace();}}@Overridepublicvoidfailed(Throwableexc,AsynchronousServerSocketChannelattachment){}});group.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);}catch(IOException|InterruptedExceptione){e.printStackTrace();}}});sThread.start();//套接字客户端AsynchronousSocketChannelclient=AsynchronousSocketChannel.open();Futurefuture=client.connect(newInetSocketAddress(InetAddress.getLocalHost(),port));future.get();ByteBufferbuffer=ByteBuffer.allocate(100);client.read(buffer,null,newCompletionHandler(){@Overridepublicvoidcompleted(Integerresult,Voidattachment){System.out.println("Clientprint:"+newString(buffer.array()));}@Overridepublicvoidfailed(Throwableexc,Voidattachment){exc.printStackTrace();try{client.close();}catch(IOExceptione){e.printStackTrace();}}});Thread.sleep(10*1000);5.总结以上基本上就是JDK8从1.0到现在版本(本文的版本)IO的核心操作,可见IO作为一个比较常见的基础功能,在发展和发展上发生了很大的变化变化,而且它变得更容易使用。IO的操作也比较容易理解。一进一出,掌握了输入输出也就掌握了IO。Socket是网络交互的综合功能。显然,NIO的复用,为Socket带来更多的活力和选择,用户可以根据自己的实际场景选择相应的代码策略