从JDK1.4开始Java引入了一系列改进的输入/输出处理的新功能,统称为NIO,即新IO,新增了许多用于处理输入输出的类,新IO采用内存映射文件的方式处理输入输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件,这种方式进行输入输出比传统的输入输出快的多
Channel通道和Buffer缓冲是NIO中的两个核心对象
Chanel是对传统输入输出系统的模拟,通过map方法可以将一块数据映射到内存中
Buffer本质是一个数组,发送到Channel中的所有对象都必须先放到Buffer中,而从Channel中读取的数据必须先放到Buffer中
IO模型
同步阻塞IO(Blocking IO):即传统的IO模型
同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库
多路复用IO(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型
异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
java NIO就是采用多路复用IO模式。在多路复用IO模型中,会有一个线程(Java中的Selector)不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
Buffer是一个抽象类,主要作用是用于装入数据,然后输出数据
最常见的子类ByteBuffer可以在底层字节数组上进行get/set操作
其它基本数据类型都有对应的Buffer类:CharBuffer、 ShortBuffer、 IntBuffer、 LongBuffer、FloatBuffer、 DoubleBuffer
静态方法static XxxBuffer allocate(int capacity)创建一个容量为capacity的XxxBuffer对象
Buffer中有3个重要概念:容量capacity、界限limit和位置position
容量capacity表示该Buffer的最大数据容量,创建后则不能改变
界限limit,位于limit后的数据既不可被读,也不可被写
位置position用于指明下一个可以被读出的或者写入缓冲区的位置索引
标记mark位置
Buffer的position为0,limit为capacity,程序可以通过put方法向Buffer写入一些数据,每放入一些数据,Buffer的position响应的向后移动。
写文件操作
String fileName = "nioFile";
try (FileOutputStream fos = new FileOutputStream(new File(fileName))) {
FileChannel channel = fos.getChannel();
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("你好你好..");
int length = 0;
while ((length = channel.write(byteBuffer)) != 0) {
System.out.println("写入长度:" + length);
}
} catch (IOException e) {
e.printStackTrace();
}
读文件操作
String fileName = "C:\IODemo\nioFile";
try (FileInputStream fis = new FileInputStream(new File(fileName));
FileChannel channel = fis.getChannel()) {
int capacity = 100;
ByteBuffer byteBuffer = ByteBuffer.allocate(capacity);
int length = -1;
while ((length = channel.read(byteBuffer)) != -1) {
byteBuffer.clear();
byte[] array = byteBuffer.array();
System.out.write(array, 0, length);
System.out.println();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Channel可以直接将文件的部分或者全部直接映射成Buffer
注意:不能直接访问Channel中的数据,包括读取、写入都不行。Channel只能与Buffer进行交互
Channel中最常用的三个方法是map()、read()和write(),map()方法将Channel对应的部分或全部数据映射成ByteBuffer然后写入数据
异常:java.nio.charset.MalformedInputException一般是编码转换时由于编码字符集错误导致的,可以修改Charset中编码字符集名称解决。例如GBK、UTF-8等
selector是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel通道的状态是否处于可读、可写。如此可以实现单线程管理多个channels也就是可以管理多个网络链接。
使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销
基本用法
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel必须是非阻塞的。
while(selector.select()>0){
Iterator<SelectionKey> it=selector.selectedKeys().iterator();
...
}
所有文件在底层都是二进制文件,字符文件是系统将底层的二进制序列转换为字符,这里会涉及编码Encoder和解码Decoder
availableCharsets():SortedMap 获取当前JDK所支持的所有字符集
Charset c=Charset.forName("GBK");
Java7新增StandardCharsets类,其中包含了ISO_8859_1、UTF_8、UTF-16等类变量,这些类变量代表了最常见的字符集对应的Charset对象
Charset c1 = Charset.forName("GBK");
CharsetEncoder encoder = c1.newEncoder();
CharsetDecoder decoder = c1.newDecoder();
CharBuffer cb = CharBuffer.allocate(8);
cb.put('孙');
cb.put('误');
cb.put('空');
cb.flip();
ByteBuffer bb=encoder.encode(cb);//将CharBuffer转换为ByteBuffer
for(int i=0;i<6;i++) {
System.out.println(bb.get(i)+" ");
}
System.out.println("====================");
System.out.println(decoder.decode(bb));//将byteBuffer转换为charBuffer
文件锁用于阻止多个进程并发修改同一个文件
锁可以是排它锁或共享锁,当shared为true时表示共享锁,允许多个进程读取文件,但是阻止其它进程获取该文件的排它锁。默认是排它锁
FileChannel channel=new FileOutputStream("a.txt").getChannel();
FileLock lock=channel.tryLock();//非阻塞式对文件加锁
lock.release();//释放锁
文件锁是建议性的,不是强制性的,就算一个程序不能获取文件锁,也可以对文件进行读写。文件锁是由Java虚拟机所持有的,关闭FileChannel时会释放Java虚拟机上该文件的所有锁
Jdk7中新增了一些与文件、网络I/O相关的一些api。这些API被称为NIO.2或称为AIO。AIO最大的一个特性就是异步能力,这种能力对socket与文件I/O都起作用。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O处理。AIO是对JDK1.4中提出的同步非阻塞NIO的进一步增强。
jdk7主要增加了三个新的异步通道:
因为AIO的实施需充分调用OS参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。因此在实际中AIO使用不是很广泛
异步无非是通知系统做一件事情,然后自己做其他事情去了。异步调用如何做后续处理。通常有两种方式:
将来式用Java.util.concurrent声明一个Future,用来保存异步操作的处理结果。通常用Future get()方法在异步IO操作完成时获取其结果。
AsynchronousFileChannel会关联线程池,它的任务是接收IO处理事件,并分发给负责处理通道中IO操作结果的结果处理器。跟通道中发起的IO操作关联的结果处理器确保是由线程池中的某个线程产生。
Path path = Paths.get("data/a1.txt"); // 从语义的角度上说比File好一些,用于封装一 个文件或者文件夹对象
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,
StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> future = channel.read(buffer, 0);
int readNum = future.get();
buffer.flip();
CharBuffer cb = CharBuffer.allocate(1024);
CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
decoder.decode(buffer,cb,false);
cb.flip();
String ss=new String(cb.array(),0,cb.limit());
System.out.println(ss);
回调式所采用的事件处理技术,基本思想是主线程会派一个CompletionHandler到独立的线程中执行IO操作。这个对象将带着IO的操作的结果返回到主线程中,这个结果会触发它自己的completed或failed方法。
在异步IO活动结束后,接口java.nio.channels.CompletionHandler会被调用,其中V是结果类型,A是提供结果的附着对象。此时必须已经有了该接口completed(V,A)和failed(V,A)方法的实现,这样程序才能知道异步IO操作成功或失败时该如何处理。
Path path=Paths.get("/data/abc.txt");
AsynchronousFileChannel channel=AsynchronousFileChannel.open(path);
ByteBuffer buffer=ByteBuffer.allocate(1024);
channel.read(buffer,0,buffer,new CompletionHandler<Integer,ByteBuffer>(){
public void completed(Integer result,ByteBuffer attachment){
System.out.println(Thread.currentThread().getName()+" read success!");
}
public void failed(Throwable ex, ByteBuffer attachment){
System.out.println("read error!");
}
});
while(true){
System.out.println(Thread.currentThread.getName()+" sleep");
Thread.sleep(1000);
}
场景分析:
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO。