JavaNIO

Java NIO

1. Java NIO 概念

Java NIO(New IO) 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

2. NIO与IO的主要区别

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
无 选择器

NIO面向缓冲区数据流通图

  • 通道负责连通,搭建缓冲区流通路径

  • 缓冲区用于来回运送数据

NIO基于通道的双向缓冲区

NIO核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

3. 缓冲区 (Buffer)

缓冲区就是数组,用于存储不同数据类型的数据。

3.1 缓冲区的类型

根据数据类型不同(除了boolean类型),提供了相应类型的缓冲区:

1
2
3
4
5
6
7
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

  • 上述各类型缓冲区管理方式几乎一致,通过allocate()获取缓冲区
    1
    ByteBuffer buf = ByteBuffer.allocate(1024);

3.2 缓冲区存取数据的两个核心方法

  • put() 存入数据到缓冲区中
  • get() 获取缓冲区中的数据

3.3 缓冲区中的四个核心属性

  1. capacity 容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变,即为数组长度。
  2. limit 界限,表示缓冲区中可以操作数据的大小。limit值后面的数据不能进行读写。
  3. position 位置,表示缓冲区中正在操作数据的位置。
  4. mark 标记 , 表示记录当前position的位置,可以通过reset()恢复到mark的位置。
  5. 0 <= mark <= position <= limit <= capacity

3.3.1 position、limit、capacity 值的关系

1530491456931

  • 当allocate(10)分配10个字节大小的缓冲区后,position的位置为0,capacity(容量)的值为10,limit(界限)值为10。

  • 当使用put(5)方法进入写数据模式时,position指针的位置在填入数据后的第一个空闲位置,此时位置为5,capacity总容量值不变,依然为10,limit界限值还是为10。

  • 当使用方法flip()后进入读数据模式,此时的position指针的位置为使用区的开始位置,即为0。limit界限的值则为5,因为在读取模式中,读取的应为使用区,所以界限为5,超过5就是空闲区了,最多取到5。capacity总容量的值依然为10变。

3.4 缓冲区Buffer类中的常用方法

Modifier and Type Method and Description
abstract Object array()返回支持此缓冲区的数组 (可选操作) 。
abstract int arrayOffset()返回该缓冲区的缓冲区的第一个元素的背衬数组中的偏移量 (可选操作) 。
int capacity()返回此缓冲区的容量。
Buffer clear()清除此缓冲区。但是其中的元素并没有消失,只是处于在 被遗忘状态,因为position、limit 值全部归零,和刚刚分配时一致。
Buffer flip()翻转这个缓冲区。
abstract boolean hasArray()告诉这个缓冲区是否由可访问的数组支持。
boolean hasRemaining()告诉当前位置和极限之间是否存在任何元素。
abstract boolean isDirect()告诉这个缓冲区是否为 direct 。
abstract boolean isReadOnly()告知这个缓冲区是否是只读的。
int limit()返回此缓冲区的限制。
Buffer limit(int newLimit)设置此缓冲区的限制。
Buffer mark()将此缓冲区的标记设置在其位置。
int position()返回此缓冲区的位置。
Buffer position(int newPosition)设置这个缓冲区的位置。
int remaining()返回当前位置和限制之间的元素数。
Buffer reset()将此缓冲区的位置重置为先前标记的位置。
Buffer rewind()倒带这个缓冲区。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
String str = "hxh";
//定义缓冲区,指定缓冲区类型和大小
ByteBuffer bf = ByteBuffer.allocate(1024);


System.out.println("--allocate()---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());

/*
--allocate()---
capacity---1024
limit---1024
position---0
*/
//利用put() 装入缓冲区,进去写模式
bf.put(str.getBytes());
System.out.println("--put()---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());

/*
--put()---
capacity---1024
limit---1024
position---3
*/
//使用flip()进入读数据模式
bf.flip();
System.out.println("--flip()---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());
/*
--flip()---
capacity---1024
limit---3
position---0
*/
//使用get(byte []) 读取缓冲区中的数据
byte[] dst = new byte[bf.limit()];
bf.get(dst);//此方法将字节从此缓冲区传输到给定的目标数组
System.out.println(new String(dst,0,dst.length));//打印字节数组中的数据
System.out.println("--get()---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());
/*
hxh
--get()---
capacity---1024
limit---3
position---3
*/

测试结果:如上图一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--allocate()---
capacity---1024
limit---1024
position---0
--put()---
capacity---1024
limit---1024
position---3
--flip()---
capacity---1024
limit---3
position---0

hxh
--get()---
capacity---1024
limit---3
position---3

rewind(),实现可重复读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用rewind方法,实现可重复读
bf.rewind();

System.out.println("--rewind() 重复读取,指针归0---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());

/*
--rewind() 重复读取,指针归0---
capacity---1024
limit---3
position---0

*/

clear()清除此缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//clear()清除此缓冲区。但是其中的元素并没有消失,
// 只是处于在 被遗忘状态,
// 因为position、limit 值全部归零,和刚刚分配时一致。
bf.clear();
System.out.println("--clear()---");
System.out.println("capacity---"+bf.capacity());
System.out.println("limit---"+bf.limit());
System.out.println("position---"+bf.position());

System.out.println("依然可以取到值,测试:"+(char) bf.get());

/*
--clear()---
capacity---1024
limit---1024
position---0
依然可以取到值,测试:h

*/

  • 注意点:clear()清除此缓冲区。但是其中的元素并没有消失,只是处于在 被遗忘状态,因为position、limit、capacity 值全部归零,和刚刚分配时一致。如果直接bf.get()取值,当然也是可以取到。

mark()和 reset()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String str = "abcdefg";
ByteBuffer bf = ByteBuffer.allocate(1024);
bf.put(str.getBytes());
bf.flip();
byte[] dst = new byte[bf.limit()];
bf.get(dst,0,2);
System.out.println(new String(dst,0,2)); // ab
System.out.println("position---"+bf.position()); //2

//此时标记一下position的位置
bf.mark();
System.out.println("此时mark一下");
bf.get(dst,2,2);
System.out.println(new String(dst,2,2)); //cd
System.out.println("position---"+bf.position()); //4

bf.reset(); //重置到标记位置
System.out.println("reset 重置后---");
System.out.println("position---"+bf.position()); //2 又回到了mark标记的位置

结果:

1
2
3
4
5
6
7
ab
position---2
此时mark一下
cd
position---4
reset 重置后---
position---2

hasRemainin 和 remaining

1
2
3
4
5
//hasRemaining  当前位置和极限位置之间是否还有元素
if(bf.hasRemaining()){
//有元素, 返回当前位置和极限位置之间的元素数量
System.out.println(bf.remaining());
}

3.5 继承Buffer类的各具体类型缓冲区中的常用方法(以ByteBuffer类为例)

3.5.1 allocate 分配缓冲区

1
public static ByteBuffer allocate(int capacity)

分配一个新的字节缓冲区

新缓冲区的位置将为零,其限制将为其容量,其标记将不定义,并且其每个元素将被初始化为零。 它将有一个backing array ,其array offset将为零。

  • 参数

    capacity - 新的缓冲区的容量,以字节为单位

  • 结果

    新的字节缓冲区

3.5.2 put 存入 ,写入缓冲区

  • 1
    public abstract ByteBuffer put(byte b)

    相对放置法(可选操作) 。

    将给定字节写入当前位置的缓冲区,然后增加位置。

    • 参数

      b - 要写入的字节

    • 结果

      这个缓冲区

  • 1
    2
    public abstract ByteBuffer put(int index,
    byte b)

    绝对put方法(可选操作) 。

    将给定字节写入给定索引的缓冲区。

    • 参数

      index - 要写入字节的索引

      b - 要写入的字节值

    • 结果

      这个缓冲区

  • 1
    2
    3
    public ByteBuffer put(byte[] src,
    int offset,
    int length)

    相对大容量put方法(可选操作) 。

    此方法将字节从给定的源数组传输到此缓冲区。 如果要从数组中复制的字节多于保留在此缓冲区中的字节数,也就是说,如果length > remaining() ,则不会传输任何字节,并抛出BufferOverflowException 。

    否则,该方法将给定数组中的length个字节复制到此缓冲区中,从阵列中的给定偏移量和该缓冲区的当前位置开始。 此缓冲区的位置然后增加length 。

    换言之,所述表格dst.put(src, off, len)的这种方法的调用具有完全一样的环相同的效果

    1
    for (int i = off; i < off + len; i++) dst.put(a[i]);

    除了它首先检查这个缓冲区中是否有足够的空间,并且它可能更有效率。

    • 参数

      src - 要读取字节的数组

      offset - 要读取的第一个字节的数组内的偏移量; 必须是非负数,不得大于array.length

      length - 要从给定数组读取的字节数; 必须是非负数,不得大于array.length - offset

    • 结果

      这个缓冲区

  • 1
    public ByteBuffer put(ByteBuffer src)

    相对大容量put方法(可选操作) 。

    此方法将给定源缓冲区中剩余的字节传输到此缓冲区。 如果源缓冲区中剩余的字节多于此缓冲区,即src.remaining() > remaining() ,则不会传输任何字节,并抛出BufferOverflowException 。

    否则,该方法将n = src.remaining()个字节从给定缓冲区复制到此缓冲区中,从每个缓冲区的当前位置开始。 然后将两个缓冲器的位置递增n 。

    换句话说,调用此方法的形式dst.put(src)具有与循环完全相同的效果

    1
    2
    while (src.hasRemaining())
    dst.put(src.get());

    除了它首先检查这个缓冲区中是否有足够的空间,并且它可能更有效率。

    • 参数

      src - 读取字节的源缓冲区; 不能是这个缓冲区

    • 结果

      这个缓冲区

3.5.3 get 获取 ,读取缓冲区

  • 1
    public abstract byte get()

    相对获取方法。 读取该缓冲区当前位置的字节,然后增加位置。

    • 结果

      缓冲区当前位置的字节

  • 1
    public abstract byte get(int index)

    绝对获取方法。 读取给定索引处的字节。

    • 参数

      index - 读取字节的索引

    • 结果

      给定索引的字节

  • 1
    public ByteBuffer get(byte[] dst)

    相对批量获取方法。

    此方法将字节从此缓冲区传输到给定的目标数组。 调用此方法的形式为src.get(a)的行为方式与调用完全相同

    1
    src.get(a, 0, a.length)
    • 参数

      dst - 目的地阵列

    • 结果

      这个缓冲区

  • ​

    1
    2
    3
    public ByteBuffer get(byte[] dst,
    int offset,
    int length)

    相对批量获取方法。

    此方法将字节从此缓冲区传输到给定的目标数组。 如果缓冲区中剩余的字节比满足请求所需的字节少,也就是说,如果length > remaining() , 则不 传输任何字节并抛出BufferUnderflowException 。

    否则,该方法将length字节从该缓冲区复制到给定的数组中,从该缓冲区的当前位置开始,并在数组中给定的偏移量。 然后将该缓冲区的位置增加length 。

    换句话说,调用此方法的形式src.get(dst, off, len)具有与循环完全相同的效果

    1
    for (int i = off; i < off + len; i++) dst[i] = src.get():

    除了它首先检查这个缓冲区中是否有足够的字节,并且它可能更有效率。

    • 参数

      dst - 要写入字节的数组

      offset - 要写入的第一个字节的数组中的偏移量; 必须是非负数,不得大于dst.length

      length - 要写入给定数组的最大字节数; 必须是非负数,不得大于dst.length - offset

    • 结果

      这个缓冲区

4. 非直接缓冲区和直接缓冲区

4.1 概念区分

  • 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中.
  • 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。操作系统的内存中,可以提升效率。

1530524051395

4.2 直接缓冲区的相对优缺点

  • 直接缓冲区利用了操作系统中直接映射方式,在物理内存中初始化一个物理内存映射文件,省去了非直接缓冲区的中间多余的内容复制步骤,让应用程序和物理磁盘直接面对。
    • 优点:简化步骤,直接映射,提升效率。
    • 缺点:
      • 初始化物理内存映射文件时,耗费较大物理内存,并且不会直接用完释放,必须通过垃圾回收机制进行释放。
      • 直接操作物理内存和磁盘,不安全,不易控制。
    • 适合:数据长时间在内存中操作,大量数据直接在物理内存中存放,不存放在jvm中。
  • 直接缓冲区,Java虚拟机会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容),即省去了中间内容复制操作。而是直接在此缓冲区上执行本机IO操作。
  • 直接缓冲区,通过Buffer类中的allocateDirect()工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。

5. 通道(Channel)

5.1 通道概念

用于源节点和目标节点的连接。NIO中负责缓冲区中数据的传输。Channel类似于传统的“流”。但是Channel本身不存储数据,因此需要配合缓冲区进行传输。

5.2 Channel的主要实现类

1
2
3
4
5
java.nio.channels.Channel 接口
|-- FileChannel
|-- SocketChannel
|-- ServletSocketChannel
|-- DatagramChannel

5.3 Channel的获取

  1. Java对于支持通道的类都提供了getChannel()方法。
    • 本地IO:
      • FileInputStream FileOutputStream
      • RandomAccessFile
    • 网路IO:
      • Socket
      • ServerSocket
      • DatagramSocket
  2. Java7中的NIO 2 对于各个通道提供了静态方法open()
  3. Java7中的NIO 2 的Files工具类提供的方法 newByteChannel()