Java SE根底坚固(六):Java IO
到当初为止,Java IO可分为三类:BIO、NIO、AIO。最早呈现的是BIO,而后是NIO,最近的是AIO,BIO即Blocking IO,NIO有的文章说是New NIO,也有的文章说是No Blocking IO,我查了一些材料,官网说的应该是No Blocking IO,提供了Selector,Channle,SelectionKey形象,AIO即Asynchronous IO(异步IO),提供了Fauture等异步操作。
1 BIO
[](https://imgchr.com/i/i81QZn)
[
](https://imgchr.com/i/i81QZn)
上图是BIO的架构体系图。能够看到BIO次要分为两类IO,即字符流IO和字节流IO,字符流即把输入输出数据当做字符来对待,Writer和Reader是其继承体系的最高层,字节流即把输入输出当做字节来对待,InputStream和OutputStream是其继承体系的最高层。上面以文件操作为例,其余的实现类也十分相似。
《2020最新Java根底精讲视频教程和学习路线!》
顺便说一下,整个BIO体系大量应用了装璜者模式,例如BufferedInputStream包装了InputStream,使其领有了缓冲的能力。
1.1 字节流
public class Main { public static void main(String[] args) throws IOException { //写入文件 FileOutputStream out = new FileOutputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); out.write("hello,world".getBytes("UTF-8")); out.flush(); out.close(); //读取文件 FileInputStream in = new FileInputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); byte[] buffer = new byte[in.available()]; in.read(buffer); System.out.println(new String(buffer, "UTF-8")); in.close(); }}复制代码
向FileOutputStream构造函数中传入文件名来创立FileOutputStream对象,即关上了一个字节流,之后应用write办法向字节流中写入数据,实现之后调用flush刷新缓冲区,最初记得要敞开字节流。读取文件也是相似的,先关上一个字节流,而后从字节流中读取数据并存入内存中(buffer数组),而后再敞开字节流。
因为InputStream和OutputStream都继承了AutoCloseable接口,所以如果应用的是try-resource的语法来进行字节流的IO操作,可不须要手动显式调用close办法了,这也是十分举荐的做法,在示例中我没有这样做只是为了不便。
1.2 字符流
字节流次要应用的是InputStream和OutputStream,而字符流次要应用的就是与之对应的Reader和Writer。上面来看一个示例,该示例的性能和上述示例的一样,只不过实现伎俩不同:
public class Main { public static void main(String[] args) throws IOException { Writer writer = new FileWriter("E:Java_projecteffective-javasrctopyeononiobiotest.txt"); writer.write("hello,worldn"); writer.write("hello,yeononn"); writer.flush(); writer.close(); BufferedReader reader = new BufferedReader(new FileReader("E:Java_projecteffective-javasrctopyeononiobiotest.txt")); String line = ""; int lineCount = 0; while ((line = reader.readLine()) != null) { System.out.println(line); lineCount++; } reader.close(); System.out.println(lineCount); }}复制代码
Writer非常简单,无奈就是关上字符流,而后向字符流中写入字符,而后敞开。要害是Reader,示例代码中应用了BufferedReader来包装FileReader,使得本来没有缓冲性能的FileReader有了缓冲性能,这就是下面提到过的装璜者模式,BufferedReader还提供了方便使用的API,例如readLine(),这个办法每次调用会读取文件中的一行。
以上就是BIO的简略应用,源码的话因为波及太多的底层,所以如果对底层不是很理解的话会很难了解源码。
2 NIO
BIO是同步阻塞的IO,而NIO是同步非阻塞的IO。NIO中有几个比拟重要的组件:Selector,SelectionKey,Channel,ByteBuffer,其中Selector就是所谓的选择器,SelectionKey能够简略了解为选择键,这个键将Selector和Channle进行一个绑定(或者所Channle注册到Selector上),当有数据达到Channel的时候,Selector会从阻塞状态中恢复过来,并对该Channle进行操作,并且,咱们不能间接对Channle进行读写操作,只能对ByteBuffer操作。如下图所示:
[](https://imgchr.com/i/i8YB80)
[
](https://imgchr.com/i/i8YB80)
上面是一个Socket网络编程的例子:
//服务端public class SocketServer { private Selector selector; private final static int port = 9000; private final static int BUF = 10240; private void init() throws IOException { //获取一个Selector selector = Selector.open(); //获取一个服务端socket Channel ServerSocketChannel channel = ServerSocketChannel.open(); //设置为非阻塞模式 channel.configureBlocking(false); //绑定端口 channel.socket().bind(new InetSocketAddress(port)); //把channle注册到Selector上,并示意对ACCEPT事件感兴趣 SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT); while (true) { //该办法会阻塞,直到和其绑定的任何一个channel有数据过去 selector.select(); //获取该Selector绑定的SelectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); //记得删除,否则就有限循环了 iterator.remove(); //如果该事件是一个ACCEPT,那么就执行doAccept办法,其余的也一样 if (key.isAcceptable()) { doAccept(key); } else if (key.isReadable()) { doRead(key); } else if (key.isWritable()) { doWrite(key); } else if (key.isConnectable()) { System.out.println("连贯胜利!"); } } } } //写办法,留神不能间接对channle进行读写操作,只能对ByteBuffer进行操作 private void doWrite(SelectionKey key) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(BUF); buffer.flip(); SocketChannel socketChannel = (SocketChannel) key.channel(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.compact(); } //读取音讯 private void doRead(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(BUF); long reads = socketChannel.read(buffer); while (reads > 0) { buffer.flip(); byte[] data = buffer.array(); System.out.println("读取到音讯: " + new String(data, "UTF-8")); buffer.clear(); reads = socketChannel.read(buffer); } if (reads == -1) { socketChannel.close(); } } //当有连贯过去的时候,获取连贯过去的channle,而后注册到Selector上,并设置成对读音讯感兴趣,当客户端有音讯过去的时候,Selector就能够让其执行doRead办法,而后读取音讯并打印。 private void doAccept(SelectionKey key) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); System.out.println("服务端监听中..."); SocketChannel socketChannel = serverChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(key.selector(), SelectionKey.OP_READ); } public static void main(String[] args) throws IOException { SocketServer server = new SocketServer(); server.init(); }}//客户端,写得比较简单public class SocketClient { private final static int port = 9000; private final static int BUF = 10240; private void init() throws IOException { //获取channel SocketChannel channel = SocketChannel.open(); //连贯到近程服务器 channel.connect(new InetSocketAddress(port)); //设置非阻塞模式 channel.configureBlocking(false); //往ByteBuffer里写音讯 ByteBuffer buffer = ByteBuffer.allocate(BUF); buffer.put("Hello,Server".getBytes("UTF-8")); buffer.flip(); //将ByteBuffer内容写入Channle,即发送音讯 channel.write(buffer); channel.close(); } public static void main(String[] args) throws IOException { SocketClient client = new SocketClient(); client.init(); }}复制代码
尝试启动一个服务端,多个客户端,后果大抵如下所示:
服务端监听中...读取到音讯: Hello,Server 服务端监听中...读取到音讯: Hello,Server 复制代码
正文写得挺分明了,我这里只是简略应用了NIO,但实际上NIO远远不止这些货色,光一个ByteBuffer就能说一天,如果有机会,我会在前面Netty相干的文章中具体说一下这几个组件。在此就不再多说了。
吐槽一些,纯NIO写的服务端和客户端切实是太麻烦了,一不小心就会写错,还是应用Netty相似的框架好一些啊。
3 AIO
在JDK7中新增了一些IO相干的API,这些API称作AIO。因为其提供了一些异步操作IO的性能,但实质是其实还是NIO,所以能够简略的了解为是NIO的裁减。AIO中最重要的就是Future了,Future示意未来的意思,即这个操作可能会继续很长时间,但我不会等,而是到未来操作实现的时候,再过去告诉我,这就是异步的意思。上面是两个应用AIO的例子:
public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> future = channel.read(buffer,0); Integer readNum = future.get(); //阻塞,如果不调用该办法,main办法会继续执行 buffer.flip(); System.out.println(new String(buffer.array(), "UTF-8")); System.out.println(readNum); }复制代码
第一个例子应用AsynchronousFileChannel来异步的读取文件内容,在代码中,我应用了future.get()办法,该办法会阻塞以后线程,在例子中即主线程,当工作线程,即读取文件的线程执行结束后才会从阻塞状态中恢复过来,并将后果返回。之后就能够从ByteBuffer中读取数据了。这是应用未来时的例子,上面来看看应用回调的例子:
public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("实现读取"); try { System.out.println(new String(attachment.array(), "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.out.println("读取失败"); } }); System.out.println("继续执行主线程"); //调用实现之后不须要期待工作实现,会间接继续执行主线程 while (true) { Thread.sleep(1000); } }}复制代码
输入的后果大抵如下所示,但不肯定,这取决于线程调度:
继续执行主线程实现读取hello,worldhello,yeonon复制代码
当工作实现,即读取文件结束的时候,会调用completed办法,失败会调用failed办法,这就是回调。具体接触过回调的敌人应该不难理解。
4 BIO、NIO、AIO的区别
- BIO是同步阻塞的IO,NIO是同步非阻塞IO,AIO异步非阻塞IO,这是最根本的区别。阻塞模式会导致其余线程被IO线程阻塞,必须期待IO线程执行结束能力继续执行逻辑,非阻塞和异步并不等同,非阻塞模式下,个别会采纳事件轮询的形式来执行IO,即IO多路复用,尽管依然是同步的,但执行效率比传统的BIO要高很多,AIO则是异步IO,如果把IO工作当做一个工作的话,在以后线程中提交一个工作之后,不会有阻塞,会继续执行以后线程的后续逻辑,在工作实现之后,以后线程会收到告诉,而后再决定如何解决,这种形式的IO,CPU效率是最高的,CPU简直没有产生过进展,而时始终至于忙状态,所以效率十分高,但编程难度会比拟大。
- BIO面向的是流,无论是字符流还是字节流,艰深的讲,BIO在读写数据的时候会依照一个接一个的形式读写,而NIO和AIO(因为AIO实际上是NIO的裁减,所以从这个方面来看,能够把他们放在一块)读写数据的时候是依照一块一块的读取的,读取到的数据会缓存在内存中,而后在内存中对数据进行解决,这种形式的益处是缩小了硬盘或者网络的读写次数,从而升高了因为硬盘或网络速度慢带来的效率影响。
- BIO的API尽管比拟底层,但如果相熟之后编写起来会比拟容易,NIO或者AIO的API抽象层次高,一般来说应该更容易应用才是,但实际上却很难“正确”的编写,而且DEBUG的难度也较大,这也是为什么Netty等NIO框架受欢迎的起因之一。
以上就是我了解的BIO、NIO和AIO区别。
5 小结
本文简略粗略的讲了一下BIO、NIO、AIO的应用,并未波及源码,也没有波及太多的原理,如果读者心愿理解更多对于三者的内容,倡议参看一些书籍,例如老外写的《Java NIO》,该书全面零碎的解说了NIO的各种组件和细节,十分举荐。