在学习NIO之前,我们需要先了解几个概念
- 什么是同步、异步IO?
同步IO是指某一线程在发出一个调用时,在调用结果没有结束之前,一直等待,调用不返回。
异步IO是指当某一线程发出一个调用时,因为不能立刻得到结果,所以该线程可以去做其它的事情,等原来的调用有了结果,以状态、通知或者回调的方式通知调用者。
- 什么是阻塞、非阻塞?
阻塞是指该线程无法做其它任务,只有条件就绪时才能继续
非阻塞是指不管IO操作是否结束,直接返回,相应操作在后台继续处理。
有的同学会混淆同步和阻塞、异步和非阻塞的概念,认为它们是等同的,其实它们还是有区别的,可以这样理解:
同步/异步的区别在在于进程是否阻塞,可以理解为实际为进程操作。
阻塞和非阻塞的区别在于应用程序的调用是否立即返回,可以理解为IO请求。
有了这些概念,我们学习下NIO与IO有哪些不同。
1、NIO与IO
翻译了jenkov的文档,总结NIO和IO如下表
IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
通过表可以看出,NIO的在原有IO的做了很大的改变,我们具体分析下
- 面向流VS面向缓冲
java IO 面向流意味着可以从流中读取一个或多个字节,这些字节不会存放在任何在任何缓存中,流中数据的流向是单向的,如果需要移动来回移动流中的数据,需要将这些数据存存放在缓冲区中。
Java NIO的面向缓冲是将数据读入到缓冲区,从缓冲区中拿数据,存放在缓冲区中的数据可以来回移动,从缓冲区拿数据时,需要先检查缓冲区是否存在自己想要的数据,当向缓冲区写数据时,要确保不要覆盖缓冲区中未处理的数据。
- 阻塞IO VS 非阻塞IO
Java IO当线程调用read()和write()时,该线程将会被阻塞,直到有一些数据读取结束或者被完全写入为止,在此期间,线程将无法执行其它任何操作。
Java NIO非阻塞模式可以从通道读取数据,并且可以获取当前可用的内容,如果当前没有可用的数据,线程可以继续处理其它的事情,而不是在数据可供读取之前保持阻塞状态。
- 选择器
Java NIO选择器允许单个线程监视多个输入通道,可以使用选择器注册多个通道,然后使用单个线程选择具有可用于处理输入的通道,或者选择已经准准备好进入写入的通道,这种选择器机制可以使用单个线程管理多个通道。
**NOTE :**IO和NIO都是同步的,因为NIO中的accept/read/write方法的内核操作都会阻塞当前线程。
2、NIO的功能
NIO主要有三个组成部分,分别为Channel(通道)、Buffer(缓冲区)、Selector(选择器)
2.1通道
通道表示打开到IO设备(如文件)的连接,类似于传统IO中的流的传输,在NIO中,Channel本身不能直接访问数据,Channel只能与Buffer进行交互,Java为Channel接口提供了四个主要的实现类
继承链为:
FileChannel | 读取、写入、映射和操作文件的通道 |
---|---|
DatagramChannel | 通过UDP读写网络中的数据通道 |
SocketChannel | 通过TCP读写网络中的数据 |
ServerSocketChannel | 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个ServerSocketChannel |
本文主要讲解FileChannel,其余三个放在网络编程的专题中讲解。
获取通道需要使用支持通道的对象调用getChannel()方法。
支持通道的类有
- FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket、ServerSocket
2.2缓冲区
缓冲区(Buffer)是一个包含特定类型的容器,在java NIO中,Buffer用于和通道进行交互,数据从通道读入缓冲区,从缓冲区写入通道。Buffer类似于Java IO中定义的缓存数组,它可以保存多个相同类型的数据,
继承链为:
继承链中的Buffer的子类仅仅是管理的数据类型不同,它们对数据处理的方法是相似的。
Buffer的子类中提供了两个用于数据操作的方法:get()和put()
- 获取buffer中的数据
get():读取单个字节
get(byte[] dst):读取多个字节到dst中
get(int index):读取指定位置的字节
//get():读取单个字节
public abstract byte get();
//get(byte[] dst):读取多个字节到dst中
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
//get(int index):读取指定位置的字节
public abstract byte get(int index);
- 将数据放入到buffer中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte() src):将 src 中的字节写入缓冲区的当前位置
put(int index,byte b):将指定字节写入缓冲区的索引位置
//put(byte b):将给定单个字节写入缓冲区的当前位置
public abstract ByteBuffer put(byte b);
//put(byte() src):将 src 中的字节写入缓冲区的当前位置
public ByteBuffer put(ByteBuffer src) {
if (src == this)
throw new IllegalArgumentException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
//put(int index,byte b):将指定字节写入缓冲区的索引位置
public abstract ByteBuffer put(int index, byte b);
打开Buffer类,可以发现主要有以下几个重要的属性
capacity:表示buffer的最大数据容量,不能为负,创建后不能修改。
limit:不能被写入或读取的第一个位置的索引,limit后的数据不能被读写
position:下一个要读取或写入数据的索引
mark:标记一个索引,通过buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过reset方法恢复到该位置。
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
缓冲区常用的方法,参考jdk8开发手册
方法 | 描述 |
---|---|
Buffer clean() | 清空缓存并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置设置为0 |
int capacity() | 返回Buffer的capacity大小 |
boolean hasRemaining() | 判断缓冲区是否还有元素 |
int limit() | 返回 Buffer 的界限(limit) 的位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置 position |
Buffer position(int n) | 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 |
int remaining() | 返回 position 和 limit 之间的元素个数 |
Buffer reset() | 将位置 position 转到以前设置的 mark 所在的位置 |
Buffer rewind() | 将位置设为 0, 取消设置的 mark |
//Buffer clear()方法,清空缓存并返回对缓冲区的引用
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
//Buffer flip()方法,将缓冲区的界限设置为当前位置,并将当前位置设置为0
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
//Buffer rewind() 将位置设为 0, 取消设置的 mark
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
//remaining()返回 position 和 limit 之间的元素个数
public final int remaining() {
return limit - position;
}
//hasRemaining,判断缓冲区是否还有元素
public final boolean hasRemaining() {
return position < limit;
}
//limit(),返回 Buffer 的界限(limit) 的位置
public final int limit() {
return limit;
}
//Buffer position(),将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
if (mark > position) mark = -1;
return this;
}
//Buffer limit(),将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
//Buffer mark(),对缓冲区设置标记
public final Buffer mark() {
mark = position;
return this;
}
//Buffer reset(),将位置 position 转到以前设置的 mark 所在的位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
缓冲区还支持分散读取和聚集写入
- 分散读取
将channel中的分散到多个buffer中,按照缓冲区的顺序,依次将Buffer填满
- 聚集写入
将多个buffer中的数据聚集到Channel,按照缓冲区的顺序,写入channel
2.3选择器
选择器部分将放在网络编程专题中讲解。
3、NIO的使用
3.1Channel的使用
-
利用通道完成文件的复制
核心代码如下
public void test1() {
FileInputStream fis = null;
FileOutputStream fos = null;
//获取通道
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fis = new FileInputStream("F:\\学习笔记\\Simon学习笔记\\java\\wife.jpg");
fos = new FileOutputStream("F:\\学习笔记\\Simon学习笔记\\java\\wife2.jpg");
inChannel=fis.getChannel();
outChannel=fos.getChannel();
//分配指定大小的缓冲器
ByteBuffer buf = ByteBuffer.allocate(1024);
//将通道中的数据存入缓冲区
while (inChannel.read(buf) != -1) {
//切换读数据模式
buf.flip();
//将缓冲区的数据写入通道
outChannel.write(buf);
//清空缓冲区
buf.clear();
}
catch(Exception e){}
finally(){}
}
-
分散读取与聚集写入
public void test2() throws IOException{ RandomAccessFile raf1=new RandomAccessFile("F:\\学习笔记\\Simon学习笔记\\java\\test.txt","rw"); //1、获取通道 FileChannel channel1=raf1.getChannel(); //分配指定大小的缓冲区 ByteBuffer buf1=ByteBuffer.allocate(512); ByteBuffer buf2=ByteBuffer.allocate(1024); //3、分散读取 ByteBuffer[] bufs={buf1,buf2}; channel1.read(bufs); for (ByteBuffer byteBuffer:bufs){ byteBuffer.flip(); } System.out.println(new String(bufs[0].array(), 0, bufs[0].limit())); System.out.println("-----------------"); System.out.println(new String(bufs[1].array(), 0, bufs[1].limit())); //4. 聚集写入 RandomAccessFile raf2 = new RandomAccessFile("F:\\学习笔记\\Simon学习笔记\\java\\test2.txt", "rw"); FileChannel channel2 = raf2.getChannel(); channel2.write(bufs); }
3.2Buffer的使用
对于Buffer,我们主要测试Buffer中的属性
- 分配指定大小的缓冲区
ByteBuffer buf=ByteBuffer.allocate(1024);
- 使用put()存入数据到缓冲区
String str="今天,你是否博学了!"
ByteBuffer buf=ByteBuffer.allocate(1024);
buf.put(str.getBytes());
- 切换读取模式
ByteBuffer.flip();
- 使用get()读取缓存中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
- 可重复读操作
ByteBuffer.rewind();
- 清空缓冲区
ByteBuffer.clear();
关注公众号:10分钟编程,获得独家整理的学习资源和日常干货推送