IO是一个复杂的东西,它包含了文件、网络、缓冲、数据等很多需要解决的功能。
因此java有很多类来解决这个难题,发展到现在,IO类库有过几次重大改变:JDK1.4添加了NIO,JDK1.7添加了AIO。
IO的五种模型
在了解IO之前我们先要清楚IO的5个模型,可以让我们知道IO的区别,这也是IO发展的根本原因。
阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象:
当用户发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪,用户就会让出cpu等待数据就绪,此时用户进程就处于阻塞状态;
当数据就绪之后,内核会将数据拷贝到用户内存,并返回结果给用户,用户进程才解除阻塞状态;
阻塞时会挂起用户线程,如果同时有多个用户进行IO操作,则需要创建多个线程来处理,造成了性能瓶颈。
非阻塞IO模型
当用户发起一个IO操作后,会主动地通过轮询方式请求内核的数据是否就绪;
这个过程非阻塞IO并不会让出CPU执行权,所以不会阻塞用户进程,而是马上就得到了一个结果;
如果结果是一个error时,它就知道数据还没有准备好,于是用户再次发送read操作;
一旦内核中的数据准备好了就把数据拷贝给用户;
非阻塞IO如果有大量用户线程进行io操作,它们会一直占用CPU请求内核数据是否就绪,导致cpu占用率很高;
另外任务完成的响应延迟增大了,因为内核数据可能在两次轮询之间完成就绪,此时需要等待用户进程再次轮询。
多路复用IO模型
内核会有一个线程不断去轮询多个socket的状态,只有当socket真正有IO事件时,才真正调用实际的IO操作;
与非阻塞IO的区别是轮询是通过内核自身的进程来执行,而非阻塞IO是通过用户进程主动发起轮询,所以效率上要比非阻塞高;
与传统的多线程+ 阻塞IO相比,多路复用IO只需要一个线程就可以管理多个socket,系统不需要建立、维护新的io线程,所以它大大减少了资源占用;
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应;
因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询;
所以多路复用IO模型的不适于处理任务较重的socket(多线程+ 阻塞IO,非阻塞IO适用),而是在于能处理大量、任务轻的socket。
信号驱动IO模型
当用户进程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户进程会继续执行;
当内核数据就绪时会发送一个信号给用户进程,用户进程接收到就绪信号之后,便在信号函数中调用IO操作。
异步IO模型
当用户发起IO操作之后,立刻就可以开始去做其它的事;
而另一方面,当内核接收到一个IO请求后会立刻返回,说明IO请求已经成功发起了,因此不会对用户进程产生任何block;
然后,内核会等待数据就绪,再进行IO操作,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它read操作完成了;
异步IO不用像信号驱动模型那样,在接收到就绪信号后再次调用IO操作,而是当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
总结
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,都是顺序执行的,需要等待内核的数据就绪;
BIO
概念
传统面向流的老IO属于阻塞IO模型,被称为BIO;
它分为两大类别:字节流与字符流;
它们的体系是通过装饰器模式来设计的,即我们在使用IO某些功能的时候需要创建多个类,装饰器的基类是FilterInputStream、FilterOutputStream,例如需要使用高效字节流:
//BufferedInputStream继承FilterInputStream BufferedInputStream bi = new BufferedInputStream(new FileInputStream("D:\java\aaa.txt"));
字节流可以转换成字符流,这是通过适配器模式实现的,适配器类有InputStreamReader、OutputStreamWriter。
Demo(多用户IO)
流程图:
Server:

package io.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * BIO服务端 * @author DaHuaiDan * */ public class Server { public static void main(String[] args) throws IOException { ServerSocket server =null; try { //创建ServerSocket,监听12345端口 server = new ServerSocket(12345); Socket socket; //处理Socket while (true) { socket = server.accept();//监听socket /** * BIO是阻塞的,所以 一个线程处理一个socket */ new Thread(new SocketHandler(socket)).start(); } } finally { if (server != null) server.close(); } } }
SocketHandler:

package io.bio; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; /** * 处理Socket线程类 * * @author DaHuaiDan * */ public class SocketHandler implements Runnable { private Socket socket; public SocketHandler(Socket socket) { this.socket = socket; } @Override public void run() { BufferedReader br =null; try { //获取客户端IO br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String message; while (true) { if ((message = br.readLine()) != null) System.out.println("server: " + message); } } catch (Exception e) { e.printStackTrace(); } finally { if (br != null) try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Client:

package io.bio; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; /** * BIO客戶端 * @author DaHuanDan * */ public class Client { public static void main(String[] args) throws IOException { int i =0; while(true){ send("massage" + i++); } } /** * 发送信息到服务端 * @param massage * @throws IOException */ private static void send(String massage) throws IOException{ Socket socket = null; PrintWriter pw = null; try{ socket = new Socket("127.0.0.1",12345); pw = new PrintWriter(socket.getOutputStream(),true); pw.print(massage); }catch(Exception e){ e.printStackTrace(); }finally{ if(pw != null) pw.close(); if(socket != null) socket.close(); } } }
总结
BIO属于阻塞IO模型,不适合多用户并发操作,但是它不同于非阻塞IO、多路复用IO需要轮询内核,所以对单个、任务较重的IO情况下,性能会好些。
NIO
概念
引用Thinging in Java的一段话:JDK1.4引入了NIO,其目的是提高速度,实际上旧IO已经使用NIO重新实现过,以便充分利用这些好处;
面向缓冲区的NIO属于多路复用IO模型,除了性能的提高,基于缓冲区操作让NIO可以在传输过程灵活的操作数据;
NIO提供了SocketChannel和ServerSocketChannel对应传统BIO模型中的Socket和ServerSocket,新增的两种通道都支持阻塞和非阻塞两种模式,适用于效率、并发等应用场景;
掌握NIO,首先我们需要了解它的三要素。
NIO的三要素
buffer相当于卡车,channel是煤矿通道,Selector监视所有通道,当selector监视到某通道有卡车从煤矿里把煤炭运输出来,我们再从卡车上获得煤炭。
Buffer 缓冲区:
读写数据都是在buffer缓冲区中处理的,缓冲区实际上是一个数组,提供了对数据访问以及维护Channel通道位置等信息;
具体的缓存区有:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer,还有MappedByteBuffer, HeapByteBuffer等,用来支持用户操作的不同场景。
Channel 通道:
Channel用于连接数据源、目的地,建立起两段的交互通道,用于Buffer在通道中运输数据,
Channel主要分两大类:SelectableChannel(用户网络读写)、FileChannel(用于文件操作)。
Selector 多路复用器:
Selector用于监视、处理多个Channel通道,原理是通过内核自身的一个单线程进行轮询;
Demo(多用户IO)
流程图:
Server:

package io.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.util.Iterator; /** * NIO服务端 * @author DaHuaiDan * */ public class Server { public static void main(String[] args) { selector(); } /** * selector用法示例 */ public static void selector() { Selector selector = null; ServerSocketChannel ssc = null; try { //打开复用器 selector = Selector.open(); //打开服务端的监听通道 ssc = ServerSocketChannel.open(); //复用器监听的端口为12345 ssc.socket().bind(new InetSocketAddress(12345)); /* * true:通道将被置于阻塞模式;false:通道将被置于非阻塞模式; * 与Selector一起使用时,Channel必须处于非阻塞模式下; * FileChannel不能切换到非阻塞模式. */ ssc.configureBlocking(false); /* * 将Channel注册到Selector上; * SelectionKey有四个参数Connect、 Accept、 Read、Write,代表Selector可以监听四种不同类型的事件 */ ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { /** * 设置select轮询时间为1s: * NIO是多路复用IO,通过selector轮询客户端socket即可,不需创建多个线程来处理多个socket: */ if (selector.select(1000) == 0) { System.out.println("服务端select无事件到达"); continue; } //获取SelectionKey事件集 Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); //处理SelectionKey事件集 SelectionKeyHandler.getSelectionKeyHandler(iter).handler(); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (selector != null) selector.close(); if (ssc != null) ssc.close(); } catch (IOException e) { e.printStackTrace(); } } } }
SelectionKeyHandler:

package io.nio; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; /** * 处理electionKey事件类 * @author DaHuaiDan * */ public class SelectionKeyHandler { private static SelectionKeyHandler selectionKeyHandler = new SelectionKeyHandler(); private static Iterator<SelectionKey> ite; private SelectionKeyHandler() { } /** * 获取SelectionKeyHandler * @param it * @return */ public static SelectionKeyHandler getSelectionKeyHandler(Iterator<SelectionKey> it) { ite = it; return selectionKeyHandler; } /** * 处理SelectionKey事件集 * @throws IOException */ public void handler() throws IOException { while (ite.hasNext()) { SelectionKey key = ite.next(); if (key.isAcceptable()) handleAccept(key); else if (key.isReadable()) handleRead(key); else if (key.isWritable() && key.isValid()) handleWrite(key); else if (key.isConnectable()) System.out.println("isConnectable = true"); ite.remove(); } } /** * 处理接入事件的函数 * @param key * @throws IOException */ public void handleAccept(SelectionKey key) throws IOException { //获取复用器的监听通道 ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel(); //通过监听获取事件发生的socket通道 SocketChannel sc = ssChannel.accept(); //设置为非阻塞的 sc.configureBlocking(false); //向Selector注册该SocketChannel,事件为read,指定该SocketChannel使用的Buffer sc.register(key.selector(), SelectionKey.OP_READ); } /** * 处理IO read函数 * @param key * @throws IOException */ public void handleRead(SelectionKey key) throws IOException { //获取复用器的监听通道 SocketChannel sc = (SocketChannel) key.channel(); //创建缓冲区Buffer ByteBuffer buf = ByteBuffer.allocate(1024); //读取数据 while(sc.read(buf) > 0){ //将读取的起始位置position重置为0 buf.flip(); System.out.println("Server: " + new String(buf.array(), 0, buf.limit())); //清空缓冲区 buf.clear(); } sc.close(); } /** * 处理IO writer函数 * @param key * @throws IOException */ public void handleWrite(SelectionKey key) throws IOException { ByteBuffer buf = (ByteBuffer) key.attachment(); SocketChannel sc = (SocketChannel) key.channel(); //发送数据到SocketChannel while (buf.hasRemaining()) sc.write(buf); buf.compact(); } }
Client:

package io.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; /** * NIO客户端 * @author DaHuaiDan * */ public class Client { public static void main(String[] args) throws IOException, InterruptedException { int i =0; while(true){ send("message" + i++); Thread.sleep(100);//防止本地socket吃不消 } } /** * NIO发送数据 * @param massage * @throws IOException */ private static void send(String massage) throws IOException { Selector selector = null; SocketChannel sc = null; try { //打开复用器 selector = Selector.open(); //打开客户端监听通道 sc = SocketChannel.open(); /* * true:通道将被置于阻塞模式;false:通道将被置于非阻塞模式; * 与Selector一起使用时,Channel必须处于非阻塞模式下; * FileChannel不能切换到非阻塞模式. */ sc.configureBlocking(false); //将Channel注册到Selector上; sc.register(selector, SelectionKey.OP_CONNECT); //连接服务器 sc.connect(new InetSocketAddress("127.0.0.1",12345)); if (sc.isConnectionPending()) { //完成连接动作 sc.finishConnect(); //发送数据到SocketChannel sc.write(ByteBuffer.wrap(massage.getBytes())); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (selector != null) selector.close(); if (sc != null) sc.close(); } catch (IOException e) { e.printStackTrace(); } } } }
总结
从流程图可以看出来,NIO的核心就是Selector:通过单个线程实现,轮询socket客户端;
之前的多路复用小节中也介绍了该IO模型的优缺点,适用于IO操作小,连接多的特点。
AIO
概念
JDK1.7加入了AIO,主要为了实现异步通道,属于异步IO模型;
AIO详细的介绍日后再补充。