1.NIO和IO的区别
首先传统的IO是面向流的。而NIO是面向缓冲区的。当使用传统IO读取数据的时候,如果数据没有准备好那么线程将会一直阻塞。但是如果使用NIO进行读取时,如果数据没有准备好,那么会直接返回到缓冲区里面已经存在的数据,不会让线程发生阻塞。另外由于NIO是面向缓冲区的,所以通过缓冲区可以读取缓冲区里面任意位置的数据。很多框架比如Netty底层就是使用了NIO的方式进行读取。
2.NIO的两个基础类 Buffer和Channel类
当使用NIO读取文件的时候,你直接操作的对象是Buffer,是先通过Channel把数据读到Buffer里面的。
当使用NIO写入文件,你是通过Channel把Buffer的数据写入到文件里面的。
下面是简答的示意图
1.Buffer类
Buffer类下面有这些分类
ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
我们可以通过这些数据类型来对Buffer进行操作
Buffer类的一些基本方法
1.分配固定大小的buffer
//分配两个字节大小的字节缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(2);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2);
//两个方法的区别是上面分配的内存是JVM里面堆上面的内存,下面分配的内存是JVM外面的直接内存来使用
allocate和allocateDirect的区别
注释里面写到allocate是分配在堆里面的,读写是还需要先把文件拷贝到内核内存里面才会进行磁盘上的读写。但是allocateDirect方法分配的内存直接就是堆外内存。这部分内存只有在JVM进行FullGc的时候才会对这个区域进行释放。
这部分堆外内存我们可以通过XX:MaxDirectMemorySize参数来控制。使用堆外内存进行读写的效率要高于堆内存几十倍。但是重点就是这部分的垃圾回收问题。如果长时间不触发FullGC,那么这部分内存怎么办。我们也可以通过代码来手动释放内存。
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2);
((DirectBuffer)byteBuffer).cleaner().clean();
//释放这部分内存 注意只有allocateDirect这种方式进行分配内存才可以调用clean方法手动释放内存
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
//上面是源码 可以看出调用allocateDirect创建的是DirectByteBuffer对象 调用allocate创建的是HeapByteBuffer对象
2.Buffer类的三个重要属性
position、limit、capacity
模式 | position | limit | capacity |
---|---|---|---|
读 | 读的位置,开始是0,逐渐增长 | 能够读的最多的大小,跟当前buffer里面数据的大小相同 ,最大值为capacity-1 | buffer的容量 |
写 | 开始写的位置,逐渐增长 | 能够写入的最大大小 跟capacity值相同 | buffer的容量 |
他们三个都是Buffer类的属性,并且提供了我们修改和读取的方法,注意无法改变capacity的值,只可以读
//修改 返回值是修改后Buffer
Buffer new=byteBuffer.limit(8);
// 读取 返回值是limit的值
int limit = byteBuffer.limit();
3.切换模式
Buffer类既可以用来读也可以用来写。他只要是空间足够就是可以直接写的,但是我们想要读的时候,就需要把上面的三个属性其中的position和limit进行修改。
上面如果理解了你就知道切换到读状态需要把position设置为0,把limit设置为现在Buffer区写模式下的position。
// 写模式转化为读模式
byteBuffer.flip();
//java源代码
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
4.数据的清理
一个Buffer就那么大,如果你一直写写满了,那么如果数据满了,就没办法继续写了。这就需要我们手动来清理Buffer。
//clear指的值清除所有的数据
//compact指的是清除已经被读过的数据
byteBuffer.clear();
byteBuffer2.compact();
5.手动初始化缓冲区
byteBuffer.put("sun".getBytes());
2.channel类
channel类似于一种管道的存在。他和Buffer相辅相成。上面的图已经画出来两者的关系了。
这个类也有下面四个分类
FileChannel 文件里面读写数据
DatagramChannel UDP协议读写数据
SocketChannel TCP协议读取数据
ServerSocketChannel 可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
这个类的方法很简单,我们先按照第一个来举例,后面我们会继续来讲。
//只有这个类型的file才可以使用channel 它和普通file的区别就是可以任意读取某个位置的数据,他是支持缓冲方式读取数据的
randomAccessFile = new RandomAccessFile(fileName, "rw");
//文件里面获取管道
FileChannel fileChannel = randomAccessFile.getChannel();
//手动向缓冲区里面写值 上面忘记写了
byteBuffer.put("sun".getBytes());
//把channel里面的值读到buffer
long b= fileChannel.read(byteBuffer);
//把buffer里面的值通过channel写入到文件
int b= fileChannel.write(byteBuffer);
3.聚集和分散
聚集指的就是把多个buffer的对象写到一个channel里面
分散指的就是把channel的数据读到多个buffer里面
//分散(scatter) 读的时候读到多个buffer里面
//聚集 gather 写的时候从多个buffer写入到一个文件
long a = fileChannel.read(new ByteBuffer[]{byteBuffer,byteBuffer2});
long b = fileChannel.write(new ByteBuffer[]{byteBuffer,byteBuffer2});
4.多个通道之间的通信
//方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数
fromChannel.transferTo(position, count, toChannel);
5.简单的例子
首先有一个txt文件 名字是test.txt 里面的内容是
abcdef博客
下面是读取的方法的代码
public void readByNio(String fileName) throws IOException {
RandomAccessFile randomAccessFile = null;
try {
//通过 RandomAccessFile 获取channel
randomAccessFile = new RandomAccessFile(fileName, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
//通过流获取channel
/* File file = new File(fileName);
FileChannel fileChannel = new FileInputStream(file).getChannel();*/
// 一个channel既可以从多个buffer里面读取,也可以写入到多个buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(2);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(2);
/* byteBuffer.put("sun".getBytes());
int b= fileChannel.write(byteBuffer);*/
//分散(scatter) 读的时候读到多个buffer里面
//聚集 gather 写的时候从多个buffer写入到一个文件
long a = fileChannel.read(new ByteBuffer[]{byteBuffer, byteBuffer2});
/* long b = fileChannel.write(new ByteBuffer[]{byteBuffer, byteBuffer2});*/
System.out.println(byteBuffer.position()+"=============="+byteBuffer2.position());
while (a != -1) {
byteBuffer.flip();
byteBuffer2.flip();
// System.out.println("当前的位置"+byteBuffer.limit());
//读取字节
//Charset这个类主要是解码和编码用的,因为我们需要读取的是字符,所以需要对字节进行解码
String line = Charset.forName("GBK").decode(byteBuffer).toString();
String line2 = Charset.forName("GBK").decode(byteBuffer2).toString();
System.out.println(line + "=============" + line2);
//clear指的值清除所有的数据
//compact指的是清除已经被读过的数据
byteBuffer.clear();
byteBuffer2.clear();
System.out.println("清理之后"+byteBuffer.position()+"++++++"+byteBuffer2.position());
a = fileChannel.read(new ByteBuffer[]{byteBuffer, byteBuffer2});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
randomAccessFile.close();
}
}
输出
2==============2
ab=============cd
清理之后0++++++0
ef=============博
清理之后0++++++0
客=============
清理之后0++++++0