这篇文章,你将得到:同步/异步+阻塞/非阻塞的性能差异;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
