NIO
导读
阻塞和非阻塞
阻塞:需要一直都在,CPU和系统资源开销大,不建议采用
非阻塞:用到了才存在,CPU和系统资源开销小,建议使用
引申:阻塞IO和非阻塞IO
1)阻塞IO:所有的Java IO流都是阻塞的,这意味着,当一条线程执行read()或者write()方法时,这条线程会一直阻塞直到读取到了一切数据或者要写出去的数据已经全部写出,在这期间这条线程不能做任何其他的事情
2)非阻塞IO:允许一条线程从channel中读取数据,通过返回值来判断buffer中是否有数据,如果没有数据,NIO不会阻塞,因为不阻塞这条线程就可以去做其他的事情,过一段时间再回来判断一下有没有数据,NIO的写也是一样的,一条线程将buffer中的数据写入channel,它不会等待数据全部写完才会返回,而是调用完write()方法就会继续向下执行,可以参考图片:NIO的读取和写入.png,通过图片可以看成NIO的读写可不是单向阻塞的,而是双向非阻塞的
友情提示:Java NIO:有阻塞模式和非阻塞模式,其中阻塞模式的NIO除了使用Buffer存储数据外和IO基本没有区别
同步和异步
同步:自己做
异步:别人帮你做
I/O模型的4种
- 同步非阻塞
- 异步阻塞
- 异步非阻塞
- 同步阻塞
BIO、NIO、AIO
BIO: 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善
可以参阅图片BIO.png 和伪异步IO.png
使用场景:当需要的连接数比较少的时候,单次发送的数据量比较大的时候,
NIO: 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
使用场景:方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂
发展历史:JDK1.4+
AIO:其实是NIO.2.0版本,意思为异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
使用场景:AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂
发展历史:JDK1.7+
小结:并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty
NIO的讲解
Java NIO和IO的主要区别:
IO NIO
面向流 面向Buffer(缓冲区)
阻塞IO 非阻塞IO
Selectors
最重要的3个概念,
Channel 通道
Buffer 缓冲区
Selector 选择器
buffer的基本讲解和三个重要属性
1.缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类 Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
2.根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:
-
ByteBuffer
-
CharBuffer
-
ShortBuffer
-
IntBuffer
-
LongBuffer
-
FloatBuffer
-
DoubleBuffer
3.友情提示:上述 Buffer 类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已
4.获取一个Buffer对象的方法:XXBuffer.allocate(int capacity);//里面的参数是为开辟的缓存区的大小
5.buffer对象的三个基本特性:
-
容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
-
限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
-
位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
buffer对象中常用的方法
Buffer 的常用方法:
方 法 描 述
Buffer clear() :清空缓冲区并返回对缓冲区的引用
Buffer flip() :将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity(): 返回 Buffer 的 capacity 大小
boolean hasRemaining(): 判断缓冲区中是否还有元素
int limit() :返回 Buffer 的界限(limit) 的位置
Buffer mark(): 对缓冲区设置标记
int position():返回缓冲区的当前位置 position
Buffer position(int n) :将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining() :返回 position 和 limit 之间的元素个数
Buffer reset() :将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() :可以在读或者写的模式写将position的值重新设置为0,limit和capacity不变, 取消设置的 mark
1.//向ButyBuffer中存入数据:put():存入数据
示例代码:
/**
* 2.@desc 向缓冲区中存入数据
*/
public static void putData(){
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//开辟一个大小为1kb的且是存储byte类型数据的缓存区
String str="IWill";
byte[] bytes = str.getBytes();//将字符串转换为字符数组
byteBuffer.put(bytes);//将数据存入到缓冲区中
System.out.println("容量:"+byteBuffer.capacity());
System.out.println("限制:"+byteBuffer.limit());
System.out.println("位置:"+byteBuffer.position());
}
2.//从ByteBuffer中取出数据:get():取出数据,flip():切换成读模式
示例代码:
/**
* 3.@desc重缓存区读取数据:可参阅buffer的基本属性图
*/
public static void getData()
{
ByteBuffer byteBuffer=ByteBuffer.allocate(10);//开辟一个大小为1kb的且是存储byte类型数据的缓存区
String str="IWill";
byte[] bytes = str.getBytes();//将字符串转换为字符数组
byteBuffer.put(bytes);//将数据存入到缓冲区中
System.out.println(byteBuffer);//1.没有切换模式的时候:pos=5 lim=10 cap=10
byteBuffer.flip();//切换成读的模式
System.out.println(byteBuffer);//2.切换模式之后的时候:pos=0 lim=5 cap=10
//3.开始读取数据
byte []bst=new byte[byteBuffer.limit()];
byteBuffer.get(bst);//以这么大长度的数据开始读入
System.out.println(byteBuffer);//pos=5 lim=5 cap=10:pos=5是因为下一个要读取的数据的索引
for(int i =0;i<byteBuffer.position();i++)//从开始的位置到下一个要读取的数据的索引之间的数据即可我们最终需要读取的数据
{
System.out.print((char)byteBuffer.get(i)+"\t");//分别取出每一个字符然后输出在控制台上
}
}
3.//rewind():可以在读或者写的模式写将position的值重新设置为0,limit和capacity不变
示例代码:
/**
* 4.@desc:rewind():可以在读或者写的模式写将position的值重新设置为0,limit和capacity不变
*/
public static void getInfoByRewmind(){
ByteBuffer byteBuffer=ByteBuffer.allocate(10);//开辟一个大小为1kb的且是存储byte类型数据的缓存区
String str="IWill";
byte[] bytes = str.getBytes();//将字符串转换为字符数组
byteBuffer.put(bytes);//将数据存入到缓冲区中
System.out.println(byteBuffer);//1.没有切换模式的时候:pos=5 lim=10 cap=10
byteBuffer.flip();//切换成读的模式
System.out.println(byteBuffer);//2.切换模式之后的时候:pos=0 lim=5 cap=10
byte []bst=new byte[byteBuffer.limit()];
byteBuffer.get(bst);//开始读取数据
System.out.println(byteBuffer);
byteBuffer.rewind();//可以在读或者写的模式写将position的值重新设置为0,limit和capacity不变
System.out.println(byteBuffer);
String str02="hhd";//并不影响下一次的存储
byteBuffer.put(str02.getBytes());
System.out.println(byteBuffer);
}
4.//clear():重置缓冲区,但是不清空里面的数据,里面的数据此时处于被遗忘状态
示例代码:
/**
* 5.@desc:clear():重置缓冲区,但是不清空里面的数据,里面的数据此时处于被遗忘状态
*/
public static void getInfoByClear()
{
//1.存入数据
ByteBuffer byteBuffer=ByteBuffer.allocate(10);
String str="IWill";
byteBuffer.put(str.getBytes());
System.out.println(byteBuffer);
//2.读取缓冲区
byteBuffer.flip();
byte []bts=new byte[byteBuffer.limit()];
byteBuffer.get(bts);
System.out.println(byteBuffer);
//3.重置缓冲区
byteBuffer.clear();
System.out.println(byteBuffer);
//4.将缓冲区中的数据打印在控制台上
System.out.println((char)byteBuffer.get(1));
}
5.//mark():标记position的位置;reset():恢复到标记的position的位置
示例代码:
/**
* 6.@desc :mark():标记position的位置;reset():恢复到标记的position的位置
*/
public static void getInfoByMark()
{
ByteBuffer byteBuffer=ByteBuffer.allocate(10);
System.out.println("最开始的缓冲区:"+byteBuffer);
//1.存入数据
String str="IWill";
byteBuffer.put(str.getBytes());
System.out.println("存入数据的缓冲区:"+byteBuffer);
//2.切换成读的模式
byteBuffer.flip();
//2.1从缓存区中读取3个数据
byte []bst=new byte[byteBuffer.limit()];
System.out.println("缓存区中的3个数据:"+byteBuffer.get(bst, 0, 3));
//3.标记positon的位置
byteBuffer.mark();//因为上一句的执行结果为:pos=3 lim=5 cap=10,所以此时mark()的作用是将pos=3的位置作为下次操作的起始的位置,于是执行完下一句的结果为:pos=5 lim=5 cap=10
System.out.println("标记后的缓存区:"+byteBuffer.get(bst, 2, 2));
//4.恢复到标记的位置
byteBuffer.reset();//恢复后的结果会与标记之前的结果一样:pos=3 lim=5 cap=10
System.out.println("恢复标记后的缓存区:"+byteBuffer);
//5.缓存区中还有能被操作的数据
if (byteBuffer.hasRemaining())
{
System.out.println("还有:"+byteBuffer.remaining()+"个");
//将他们取出来打印在控制台上
for (int i = byteBuffer.position(); i <byteBuffer.limit(); i++)
{
System.out.println((char)byteBuffer.get(i));
}
}
}
直接与非直接缓冲区
缓冲区可以分为两类:
1.非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
执行流程和原理剖析:
当我们的程序想要从硬盘中读取数据 需要
- 先从物理硬盘把数据读取到物理内存中
- 再将内容复制到JVM的内存中
- 然后读取应用程序才可以读取到内容
ps:其中传统的io和 nio的**accocate()**都是非直接缓冲区
2.直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
执行流程和原理剖析:直接缓冲区的是图中红线所标识的 直接在应用程序和物理磁盘中直接在内存中建立一个缓冲区在物理内存中,这样省略了复制的步骤 效率由此提高
channel的基本讲解
1.Channel:中文意思为“通道/海峡/频道” 由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据,Channel 只能与Buffer 进行交互。
友情提示:Channel 负责传输, Buffer 负责存储
2.Java 为 Channel 接口提供的最主要实现类如下:
1)FileChannel:用于读取、写入、映射和操作文件的通道。
2)DatagramChannel:通过 UDP 读写网络中的数据通道。
3)SocketChannel:通过 TCP 读写网络中的数据通道。
4)ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
3.怎样获取通道:
3.1获取通道的第一种方法:使用支持通道的对象调用 getChannel() 方法
3.1.1支持通道的对象的类有哪些:
本地 IO:FileInputStream/FileOutputStream ,RandomAccessFile
网络IO : Socket, ServerSocket, DatagramSocket
3.1.2那么获取通道就会变的很简单了:例如:FileChannel inputChanner = inputStream.getChannel();
3.2获取通道的第二种方法:通过通道的静态方法 open() 打开并返回指定通道:
例如 FileChannel inChannel=FileChannel.open(Paths.get(“E:/123.mp4”), StandardOpenOption.READ);
3.3获取通道的第三种方法:在 JDK 1.7 中的 NIO2.0 的 Files 工具类的 newByteChannel()
NIO实例应用之实现大文件的复制
/**
* 1.@desc 利用直接缓冲区+NIO(channel+buffer)实现大文件的复制
* @throws IOException
*/
public static void copyBigFileByChannel() throws IOException
{
long start = System.currentTimeMillis();
//1.创建直接缓冲区
ByteBuffer buffer=ByteBuffer.allocateDirect(1024);
//2.创建输入和输出流对象,进而获取到Channel对象
FileInputStream inputStream=new FileInputStream("E:/123.mp4");
FileOutputStream outputStream=new FileOutputStream("D:/234.mp4");
//3.通过流获取Channel对象
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
//4.复制
while ((inputChannel.read(buffer))!=-1)
{
buffer.flip();//切换成读的模式
outputChannel.write(buffer);
buffer.clear();
}
//5.关闭对象
outputChannel.close();
inputChannel.close();
outputStream.close();
inputStream.close();
long stop = System.currentTimeMillis();
System.out.println("本次复制耗时为:"+(stop-start));
}
通道之间的相互通信实现大文件的复制(升级版)
/**
* 2.@desc 利用通道间相互通信实现大文件的复制
* @throws Exception
*/
public static void copyBigFileByChannelTransferForm() throws Exception
{
long start =System.currentTimeMillis();
//1.构建输出和输入通道
FileChannel inChannel=FileChannel.open(Paths.get("E:/123.mp4"), StandardOpenOption.READ);
FileChannel outChannel=FileChannel.open(Paths.get("D:/123.mp4"), StandardOpenOption.CREATE,StandardOpenOption.READ,StandardOpenOption.WRITE);
//2.通道间相互通信
outChannel.transferFrom(inChannel, 0, inChannel.size());
//3.关闭对象
outChannel.close();
inChannel.close();
long stop=System.currentTimeMillis();
System.out.println("本次复制耗时为:"+(stop-start));
}
分散(Scatter)和聚集(Gather)
分散读取(Scattering Reads):是指从 Channel 中读取的数据“分散”到多个 Buffer 中,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-slubEz23-1607911570907)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20201102172547917.png)]
聚集写入(Gathering Writes): 是指将多个 Buffer 中的数据“聚集”到 Channel,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c7VBgAT0-1607911570909)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20201102172637181.png)]
实例:
/**
* 3.@desc 分散的读和聚集的写实现文件的复制,但是经常被用来实现文本文件的复制和为了提高效率的超大非文本文件的复制(例如视频,因为这样做效率更高)
* @throws Exception
*/
public static void copyFileByScatterAndGather() throws Exception
{
long start =System.currentTimeMillis();
//1.创建多个buffer
ByteBuffer byteBuffer01=ByteBuffer.allocate(1024);
ByteBuffer byteBuffer02=ByteBuffer.allocate(1024);
ByteBuffer [] bffs={byteBuffer01,byteBuffer02};
//2.分散的读取
File file=new File("E:/sg.txt");
RandomAccessFile readFile=new RandomAccessFile(file, "rw");//r:只读;w:只写;rw:读和写
FileChannel readChannel = readFile.getChannel();
//4.聚集的写
File fileTo=new File("D:/sg.txt");
RandomAccessFile writeFile=new RandomAccessFile(fileTo, "rw");
FileChannel writeChannel = writeFile.getChannel();
while ((readChannel.read(bffs))!=-1)
{
for(int i =0;i<bffs.length;i++)
{
bffs[i].flip();//3:切换模式:如果不切换模式就不能成功
}
writeChannel.write(bffs);
for(int i =0;i<bffs.length;i++)
{
bffs[i].clear();
}
}
writeChannel.close();
readChannel.close();
long stop=System.currentTimeMillis();
System.out.println("本次复制耗时为:"+(stop-start));
}
FileChannel writeChannel = writeFile.getChannel();
while ((readChannel.read(bffs))!=-1)
{
for(int i =0;i<bffs.length;i++)
{
bffs[i].flip();//3:切换模式:如果不切换模式就不能成功
}
writeChannel.write(bffs);
for(int i =0;i<bffs.length;i++)
{
bffs[i].clear();
}
}
writeChannel.close();
readChannel.close();
long stop=System.currentTimeMillis();
System.out.println("本次复制耗时为:"+(stop-start));
}