【JAVA IO】JAVA NIO源码浅析

【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的类图如下:

image.png

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 类图如下:

image.png

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 的类图如下:

image.png

前面描述过,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

你可能感兴趣的:(【JAVA IO】JAVA NIO源码浅析)