到如今为止,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等异步操做。java
上图是BIO的架构体系图。能够看到BIO主要分为两类IO,即字符流IO和字节流IO,字符流即把输入输出数据当作字符来看待,Writer和Reader是其继承体系的最高层,字节流即把输入输出当作字节来看待,InputStream和OutputStream是其继承体系的最高层。下面以文件操做为例,其余的实现类也很是相似。shell
顺便说一下,整个BIO体系大量使用了装饰者模式,例如BufferedInputStream包装了InputStream,使其拥有了缓冲的能力。编程
public class Main {
public static void main(String[] args) throws IOException {
//写入文件
FileOutputStream out = new FileOutputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
out.write("hello,world".getBytes("UTF-8"));
out.flush();
out.close();
//读取文件
FileInputStream in = new FileInputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.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方法了,这也是很是推荐的作法,在示例中我没有这样作只是为了方便。缓存
字节流主要使用的是InputStream和OutputStream,而字符流主要使用的就是与之对应的Reader和Writer。下面来看一个示例,该示例的功能和上述示例的同样,只不过实现手段不一样:服务器
public class Main {
public static void main(String[] args) throws IOException {
Writer writer = new FileWriter("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
writer.write("hello,world\n");
writer.write("hello,yeonon\n");
writer.flush();
writer.close();
BufferedReader reader = new BufferedReader(new FileReader("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.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的简单使用,源码的话由于涉及太多的底层,因此若是对底层不是很了解的话会很难理解源码。架构
BIO是同步阻塞的IO,而NIO是同步非阻塞的IO。NIO中有几个比较重要的组件:Selector,SelectionKey,Channel,ByteBuffer,其中Selector就是所谓的选择器,SelectionKey能够简单理解为选择键,这个键将Selector和Channle进行一个绑定(或者所Channle注册到Selector上),当有数据到达Channel的时候,Selector会从阻塞状态中恢复过来,并对该Channle进行操做,而且,咱们不能直接对Channle进行读写操做,只能对ByteBuffer操做。以下图所示:框架
下面是一个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相似的框架好一些啊。
在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_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.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_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.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,world
hello,yeonon
复制代码
当任务完成,即读取文件完毕的时候,会调用completed方法,失败会调用failed方法,这就是回调。详细接触过回调的朋友应该不难理解。
以上就是我理解的BIO、NIO和AIO区别。
本文简单粗略的讲了一下BIO、NIO、AIO的使用,并未涉及源码,也没有涉及太多的原理,若是读者但愿了解更多关于三者的内容,建议参看一些书籍,例如老外写的《Java NIO》,该书全面系统的讲解了NIO的各类组件和细节,很是推荐。