【JAVA IO】JAVA NIO源码浅析
JAVA NIO 是 JDK在1.4版本中发布的一套API,这套API采用了不同的方式进行了IO操作,主要包括四个部分:
- Buffers,数据容器,包路径一般为java.nio.*
- Charsets 和 相关的编码解码器,用于byte和Unicode字符之间编解码,包路径为java.nio.charset.*
- Channels 代表和实体之间的连接,可以进行IO操作。包路径为java.nio.channel.*
- Selectors,和可选择的通道配合,实现多路复用和IO非阻塞的能力。包路径为java.nio.channel.*
这里没有提到的是还有一个包路径为 java.nio.file.* ,这个是在文件系统方面提供了一些接口和实现,since 1.7 ,实际上这个属于 NIO2的部分。
一般而言,NIO 狭义上指的是 JSR 51 (www.jcp.org/en/jsr/detail?id=51 )规范,实现的关于现代操作系统上IO操作的支持和简化。在JDK1.4中实现,包括Buffers, Charsets,Channels,Selectors,以及Regular expressions 。而NIO2 是指 在JSR 203 (www.jcp.org/en/jsr/detail?id=203 ),克服传统File类的一些问题,并且支持异步IO和socket通道的功能。在JDK1.7中实现,主要包括 增强的文件系统接口,异步IO,完善了Socket 通道功能。
Buffer
Buffer 是存放基本数据的容器,Buffer类本身是一个抽象类,具体实现有ByteBuffer, CharBuffer,
DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。除了boolean,每个基本类型都有一个实现。还有HeapByteBuffer
Buffer本身是为了读写而存在的,有几个关键变量,position, limit, capacity 和 mark。
pos代表当前读或者写的位置
limit代表有效数据上界,
mark代表标记的位置
capacity代表整个容量
并且下面条件始终成立,否则抛出异常
0 <= mark <= pos <= lim <= cap;
有几个常规操作,put,get,flip,rewind,reset/mark, clear。
一般写完了,flip进行读,重读就rewind,再clear继续去写, 如果没有读完,就compact
还有一个特殊的Buffer是DirectByteBuffer ,这个Buffer的类图如下:
DirectByteBuffer 只有包访问权限,可以通过ByteBuffer的静态方法allocateDirect
工厂方法创建,这个Buffer的特点是不在堆上分配,而是使用直接内存,所以是连续的,而不受垃圾回收的影响,底层操作系统可以使用原生IO操作对这块Buff直接进行填写和清空,效率非常高。并且在使用channel的时候如果使用的不是Direct的buffer,channel底层会创建一个临时的direct buffer ,把内容拷贝进行进行操作。
另外MappedBuffer,是可以把底层文件直接映射到内存进来的,对这一段buff的操作会直接对底层文件生效。
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 50, 100);
channel
channel对象代表一种连接,包括文件,socket,硬件设备,应用组件等一系列可以进行读写IO操作的实体。Channel能够高效的在buffer和这些实体之间传输速度。Channel可以很好的对,操作系统的设施进行建模。
channel常见的实现有FileChannel,SocketChannel,DatagramChannel 类图如下:
InterruptibleChannel 接口支持异步关闭和IO阻塞可中断,AbstractInteruptibleChannel给出了初步的实现,利用begin和end进行组合,代表一个I/O操作。源码如下:
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
open = false;
interrupted = target;
try {
(略).this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
blockedOn(interruptor);
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
protected final void end(boolean completed)
throws AsynchronousCloseException
{
blockedOn(null);
Thread interrupted = this.interrupted;
if (interrupted != null && interrupted == Thread.currentThread()) {
interrupted = null;
throw new ClosedByInterruptException();
}
if (!completed && !open)
throw new AsynchronousCloseException();
}
interruptor相当于一个中断回调,blockedOn是把这个回调绑定到当前线程上,
最后判断,是不是已经被中断了,被中断了,就执行回调。
回调做的事情:关闭当前Channel,并把被中断的线程记录到channel对象interrupted域。
而end,先把回调和本线程割裂,那么本线程就不会执行中断回调,进一步在判断channel是不是已经中断过了,中断的线程是不是自己,如果发现自己已经被中断过了,抛出中断关闭,否则判断channel是不是在不完整工作的情况下关闭了,抛出异步关闭。
FileChannel可以获得文件锁,实际上文件锁是对应到底层文件系统的,可以分段加锁,选择是否共享,共享的情况下都可以读,独占的情况下,只有一个线程可以进行写操作。
package com.shalk.jio;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class ChannelLock {
public static final int QUERY_LOOP = 15000;
public static final int UPDATE_LOOP = 15000;
// 加锁长度
public final static int lockLen = 16;
public static void update(FileChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(lockLen);
IntBuffer intBuffer = buffer.asIntBuffer();
int count = 0;
int pos = 0;
for (int i = 0; i < UPDATE_LOOP; i++, pos += lockLen, count++) {
FileLock lock = channel.lock(pos, lockLen, false);
System.out.println("获得独占锁");
try {
intBuffer.clear();
int a = count;
int b = count * 2;
int c = count * 3;
int d = count * 4;
intBuffer.put(a);
intBuffer.put(b);
intBuffer.put(c);
intBuffer.put(d);
System.out.println(String.format("写入: %d %d %d %d", a, b, c, d));
buffer.clear();
channel.write(buffer, pos);
} finally {
lock.release();
}
}
}
public static void main(String[] args) throws IOException {
boolean writeMode = false;
if (args.length != 0) {
writeMode = true;
}
try (
RandomAccessFile file = new RandomAccessFile("tmp", writeMode ? "rw" : "r");
FileChannel channel = file.getChannel();
) {
if (writeMode) {
System.out.println("写模式:");
update(channel);
} else {
System.out.println("读模式:");
query(channel);
}
}
}
private static void query(FileChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(lockLen);
IntBuffer intBuffer = buffer.asIntBuffer();
int pos = 0;
int count = 0;
for (int i = 0; i < QUERY_LOOP; i++) {
FileLock lock = channel.lock(pos, lockLen, true);
System.out.println("获得共享锁");
try {
buffer.clear();
channel.read(buffer, pos);
int a = intBuffer.get(0);
int b = intBuffer.get(1);
int c = intBuffer.get(2);
int d = intBuffer.get(3);
System.out.println(String.format("读到: %d %d %d %d", a, b, c, d));
if (a != count || 2 * a != b || 3 * a != c || 4 * a != d) {
System.err.println("数据错误");
break;
}
count++;
pos += lockLen;
} finally {
lock.release();
}
}
}
}
再看一下Socketchannel 的类图如下:
前面描述过,InterruptibleChannel主要保证阻塞IO可中断,那SelectableChannel 和NetworkChannel的作用是什么呢。并且这ServerSocketChannel实现了 ByteChannel, GatheringByteChannel, ScatteringByteChannel,因此具备可读可写IO,以及gather、scatter IO的能力。而且这些Channel实现是线程安全的。
SelectableChannel 是允许Channel选择模式,阻塞模式或者非阻塞模式,非阻塞的时候可以做一些其他的事情,这个在Selector的部分再说。
另外Pipe是NIO关于管道的实现,也可以配置非阻塞,其中有两个Channel,SourceChannel和SinkChannel因为继承了SelectableChannel,都可以配置,分别是可读和可写的。
Selector
继承自SelectableChannel的Channel,像PipedChannel和SocketChannel都是可以配置非阻塞的,但是仅仅是非阻塞,无法判断数据是不是到达,例如read方法,在非阻塞下,可能返回0,可能返回数据,最好是得知数据可以读了再进行读,不然编程模型会非常复杂,即read的时候要做判断,并且要处理读到的数据。需要一个判断可以读了方法,并且如果在服务器端,有很多客户端的SocketChannel,那需要判断哪些channel没有阻塞了,可以进行处理了,Selector做了一个轮训封装,并且实现多路服用,即一个线程处理多个channel.
套路如下:
while (true)
{
int numReadyChannels = selector.select();
if (numReadyChannels == 0)
continue; // there are no ready channels to process
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext())
{
SelectionKey key = keyIterator.next();
if (key.isAcceptable())
{
// A connection was accepted by a ServerSocketChannel.
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
if (client == null) // in case accept() returns null
continue;
client.configureBlocking(false); // must be nonblocking
// Register socket channel with selector for read operations.
client.register(selector, SelectionKey.OP_READ);
}
else
if (key.isReadable())
{
// A socket channel is ready for reading.
SocketChannel client = (SocketChannel) key.channel();
// Perform work on the socket channel.
}
else
if (key.isWritable())
{
// A socket channel is ready for writing.
SocketChannel client = (SocketChannel) key.channel();
// Perform work on the socket channel.
}
keyIterator.remove();
}
}
注意事项,处理过的要remove,不然下次状态不会被更新,并且始终在selectedKeys集合内,异常处理的Channel,应该把key cancel掉,否则有可能轮训出这些已经异常的channel。
正则/Charset/Formater
NIO中不仅对非阻塞方面进行了增强,还增加了正则、字符集以及Formatter方面。
正则可以参考正则方面的。
字符集主要是对应编解码,并且Charset的decode和encode方法很强大,可以配合Buffer使用。
Formatter就是对应C语言中的printf,实现了String.format。
小结
Buffer提供了缓存的抽象和灵活操作,Channel提供了不同IO实体的连接抽象,并提供了非阻塞的实现,Selector进一步通过不同操作系统提供了SelectorProvider提供实现,近似于select/epoll的非阻塞轮询模型,实现多路复用的效果。
参考
https://docs.oracle.com/javase/8/docs/api/java/nio/package-summary.html
https://docs.oracle.com/javase/8/docs/api/java/nio/channels/package-summary.html#multiplex