1、简介
- 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能。
NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
-
基于缓存区
- 它一个buffer
-
基于通道
- 它有一个channel
NIO与IO的区别
- 区别一
- IO流是基于Stream(流)
- NIO它是面向缓冲区及基于通道的IO操作 - 区别二
- IO流是基于阻塞
- NIO它是非阻塞
NIO将以更加高效的方式对文件进行读写操作
缓冲区(Buffer)
什么是缓冲区
-
一个用于特定基本数据类型的容器。与通道Channel打交道
-
作用: Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
-
通道(Channel)就像铁轨,缓冲区(Buffer)就像矿车,
Buffer的基本属性
-
容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
-
限制 (limit) :第一个不应该读取或不应该写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
-
位置 (position): :下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
缓冲区的用法
NIO类下有个类叫Buffer,Buffer类下又有ByteBuffer(字节缓冲区)等
package bytebuffer用法;
import java.nio.ByteBuffer;
import org.junit.Test;
public class MainTest {
//单元测试用法:ByteBuffer用法
@Test
public void testBuffer()
{
//创建分配容量长度为10的字节缓冲区,缓冲区名为buf
ByteBuffer buf=ByteBuffer.allocate(10);
//allocate()是静态方法,不需要依赖类的创建,直接类名点静态方法名
System.out.println("position="+buf.position()); //0,刚开始操作缓冲区的位置在0下标
System.out.println("limit="+buf.limit()); //10
System.out.println("capacity="+buf.capacity()); //10
String putMessage="abcd";
//将上面的字符串转换成字节数组cbuf
byte []cbuf=putMessage.getBytes();
//上面创建的字节缓冲区buf调用put方法,将字节数组cbuf的元素放入缓冲区buf中
//从字节数组cbuf的下标为0的元素开始到数组的长度
buf.put(cbuf, 0, cbuf.length);
//你已经把abcd放到缓冲区中了,abcd对因美国下标0123
//此时缓冲区操作指向的位置是4
System.out.println("position="+buf.position()); //4
System.out.println("limit="+buf.limit()); //10
System.out.println("capacity="+buf.capacity()); //10
//从缓冲区读取数据
//方式1:调用get方法单个或者多个get
//将position值设置为0,使从第一个元素取出(你上面的position已经指向4了,
//但是字节缓冲区下标为4的位置是空的,上面我给缓冲区的容量是10)
/* buf.position(0);
System.out.println((char)buf.get());//a
System.out.println((char)buf.get());//b
System.out.println((char)buf.get());//c
System.out.println("position="+buf.position()); //现在position为3(有个元素d)
System.out.println("limit="+buf.limit()); //10
System.out.println("capacity="+buf.capacity()); //10
*/
//方式2:翻转成读模式
buf.flip();
/* flip()方法的源码
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;//这句可不看,别纠结
} */
System.out.println("position="+buf.position()); //4
System.out.println("limit="+buf.limit()); //10
System.out.println("capacity="+buf.capacity()); //10
byte cbuf2[]=new byte[buf.limit()];
//从缓存区读取数据
System.out.println("从缓存区读取数据");
buf.get(cbuf2, 0, cbuf2.length);
System.out.println(new String(cbuf2,0,cbuf2.length));
}
}
上面的例子单单是Buffer的操作,只是演示如何使用,其实一般Buffer都与channel结合使用才会高效
通道(Channel)
Channel就是表示 IO 源与目标打开的连接,通过Channel可以将数据讲到缓冲区,也就是说Channel直接能够与缓冲区进行读、写操作。
2、Channel有哪些?
- FileChannel:用于读取、写入、映射和操作文件的通道。
- 以下都是针对网络通讯的通道
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
从这里可以看出,NIO其实更多的用于网络编程。
这里只讲FileChannel。其他会在网络编程再讲。
FileChannel的用法
针对目标文件打开的连接,建立一个通道
用法一:读取文件到内存
package FileChannel用法;
import org.junit.Test;
public class MainTest {
@Test
public void testFileChannel01() throws IOException
{
//创建FileInputStream对象
FileInputStream fileInputStream=new FileInputStream(new File("a.txt"));
//获取FileChannel对象
FileChannel fc=fileInputStream.getChannel();
//定义一个ByteBuffer
ByteBuffer buf=ByteBuffer.allocate(12);
//position=0,limit=12,cap=12
long len=-1;
while((len=fc.read(buf))!=-1)
{
//设置ButeBuffer为读取模式
buf.flip();
String message=new String(buf.array(),0,buf.limit());
System.out.print(message);
/*byte []cbuf=new byte[buf.limit()];
buf.get(cbuf, 0, cbuf.length);
String message=new String(cbuf,0,cbuf.length);
System.out.print(message);*/
buf.clear();
}
}
}
用法二:复制文件功能
- 通过IO流复制文件内容
文件1复制到文件2
@Test
public void testCopyFile01() throws IOException
{
long start = System.currentTimeMillis();
FileInputStream srcFileInputStream=new FileInputStream(new File("D:\\1.mp4"));//源文件
FileOutputStream destFileOutputStream=new FileOutputStream(new File("D:\\2.mp4"));//复制到的目标文件
//获取针对1.mp4文件的FileChannel对象
FileChannel fc=srcFileInputStream.getChannel();
//获取针对2.mp4文件的FileChannel对象(看上图)
FileChannel destFc=destFileOutputStream.getChannel();
//创建ByteBuffer对象
//使用非直接方式创建ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(1024);
//给缓冲区buf的空间为1k。上面说过缓冲区看以看作矿车,缓冲区buf文件1拿了1k的数据
//搬到文件2,搬完后又再从文件1继续般1k的数据到文件2
while(fc.read(buf)!=-1)
{
//配置ByteBuffer的可读模式
buf.flip();
destFc.write(buf);//边读边写到文件
//清除缓存区
//position=0;
//limit=cap
buf.clear();
}
if(fc!=null)
{
fc.close();
}
if(destFc!=null)
{
destFc.close();
}
if(srcFileInputStream!=null)
{
srcFileInputStream.close();
}
if(destFileOutputStream!=null)
{
destFileOutputStream.close();
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
- 通过channel 的映射内存实现文件复制功能
@Test
public void testCopyFile02() throws IOException
{
long start = System.currentTimeMillis();
//建立1.mp4的通道(就是连接点)
FileChannel fileChannelA=FileChannel.open(Paths.get("d:/1.mp4"), StandardOpenOption.READ);
FileChannel fileChannelB=FileChannel.open(Paths.get("d:/2.mp4"),
StandardOpenOption.WRITE,
StandardOpenOption.READ,
StandardOpenOption.CREATE);
//建立fileChannelA通道的映射内存
MappedByteBuffer inMappedBuf = fileChannelA.map(MapMode.READ_ONLY, 0, fileChannelA.size());
//建立fileChannelB通道的映射内存
MappedByteBuffer outMappedBuf = fileChannelB.map(MapMode.READ_WRITE, 0, fileChannelA.size());
//创建一个字节数组
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
//关闭通道
fileChannelA.close();
fileChannelB.close();
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
- 支持channel之间的数据复制功能
@Test
public void testCopyFile03() throws IOException
{
long start = System.currentTimeMillis();
FileChannel fileChannelA=FileChannel.open(Paths.get("d:/1.mp4"), StandardOpenOption.READ);
FileChannel fileChannelB=FileChannel.open(Paths.get("d:/2.mp4"),
StandardOpenOption.WRITE,
StandardOpenOption.READ,
StandardOpenOption.CREATE);
fileChannelB.transferFrom(fileChannelA, 0, fileChannelA.size());
//关闭通道
fileChannelA.close();
fileChannelB.close();
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
- 通过以上三个方式的性能对比,用法二的最后一种不仅代码少效率还高,中间次之,第一种最慢。