BIO & NIO & AIO
(〇)同步vs异步、阻塞vs非阻塞
同步vs异步
- 同步:发起一个调用后,被调用者未处理完请求之前,调用不返回
- 异步:发起一个调用后,立即收到被调用者的回应表示已经接收到请求,但是被调用者没有返回结果,会通过事件/回调等机制通知调用者结果
阻塞vs非阻塞
- 阻塞:调用方必须等待请求返回结果(即被挂起),不能从事其他任务
阻塞I/O线程处于线程的哪个状态呢?
答案:RUNNABLE。因为它既没有等待notify()/notifyAll(),也不是由于synchronized被阻塞
线程池实现了伪异步I/O,但是底层仍然是
BIO。
- 非阻塞:调用方在等待请求返回结果时,不会被挂起,可以从事其他任务
在网上看到了一个很贴切的比喻——烧开水:
1.你妈妈叫你烧开水,你小时候比较笨,坐在开水壶前等水开,这是同步阻塞;
2.等你大一点了,知道等水烧开的这段时间可以忙别的事情,你只需要时不时来看看水烧开了没有,这是同步非阻塞;
3.后来你们家用上了水烧开会发出声音的壶,这样你就可以去干别的事情了,等到水开了它自己会发出声音提醒你,这就是异步非阻塞
(一)BIO
同步阻塞I/O,数据的读取写入必须阻塞在一个线程内完成,不能应付高并发。
BIO设计的类有40多个,看似杂乱,但是其实都是从4个抽象类中派生来的。
Reader:字符输入流InputStream:字节输入流Writer:字符输出流OutputStream:字节输出流
分类
1.字符读取 - Reader
- 节点流:
- 文件操作:
FileReader - 管道操作:
PipedReader - 数组操作:
CharArrayReader
- 处理流:
- 缓冲操作:
BufferedReader - 转化控制:
InputStreamReader
2.字节读取 - InputStream
- 节点流:
- 文件操作:
FileInputStream - 管道操作:
PipedInputStream - 数组操作:
ByteArrayInputStream
- 处理流:
- 缓冲操作:
BufferedInputStream - 基本数据类型操作:
DataInputStream - 对象序列化操作:
ObjectInputStream SequenceInputStream
3.字符写出 - Writer
- 节点流:
- 文件操作:
FileWriter - 管道操作:
PipedWriter - 数组操作:
CharArrayWriter
- 处理流:
- 缓冲操作:
BufferedWriter - 转化控制:
OutputStreamWriter - 打印控制:
PrintWriter
4.字节写出 - OutputStream
- 节点流:
- 文件操作:
FileOutputStream - 管道操作:
PipedOutputStream - 数组操作:
ByteArrayOutputStream
- 处理流:
- 缓冲操作:
BufferedOutputStream - 基本数据类型操作:
DataOutputStream - 对象序列化操作:
ObjectOutputStream - 打印操作:
PrintStream
demo
//BIO:try-catch-finally
BufferedReader br = null;
String sCurrentLine = null;
try{
br = new BufferedReader(new FileReader("C:\\Users\\19047535\\IdeaProjects\\batal\\src\\io\\text.txt"));
while ((sCurrentLine = br.readLine()) != null) {
System.out.println(sCurrentLine);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try{
if (br != null)
br.close();
}catch (IOException e){
e.printStackTrace();
}
}
使用try-with-resources取代try-catch-finally:
//BIO:try-with-resource
try(BufferedReader br2 = new BufferedReader(
new FileReader("C:\\Users\\19047535\\IdeaProjects\\batal\\src\\io\\text.txt"))) {
while ((sCurrentLine = br2.readLine()) != null) {
System.out.println(sCurrentLine);
}
}catch (IOException e){
e.printStackTrace();
}
缺点 & 解决方案:线程池
每一个请求需要一个I/O线程进行处理,同时线程的创建、销毁需要消耗系统资源,加重了系统的负担。
解决方法:通过线程池复用线程
线程池本质上仍然是
BIO
(二)NIO
同步非阻塞I/O,支持高并发、高负载。
应用操作之后直接返回,不会堵塞在那里。
NIO 与 BIO 的区别
BIO是面向流的,NIO是面向缓冲区的BIO是阻塞的,NIO是非阻塞的NIO有选择器而BIO没有
读 & 写
- 读:创建一个缓冲区,然后请求通道读取数据
- 写:创建一个缓冲区,填充数据,然后要求通道写入数据
核心组件
1.缓冲区(Buffer)
Buffer的本质是一块内存区域,用于和Channel进行交互——将Buffer中的数据写入Channel,或是从Channel读取数据到Buffer。
(1)属性
capacity:buffer容量,一旦设置就不能更改position:下一个读/写元素的位置,通过get/put更新limit:buffer中不能读/写的第一个元素,换句话来说就是buffer中存活元素的个数mark:buffer中一个记录的位置,调用mark()可以让mark = position,调用reset()可以让position = mark
0 <=
mark<=position<=limit<=capacity
(2)常见方法
Buffer clear()Buffer flip()Buffer rewind()Buffer position(int newPosition)
(3)使用缓冲区
- 分配缓冲区:
Buffer.allocate(capacity)
//字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(28);
//字符缓冲区
CharBuffer charBuffer = CharBuffer.allocate(100);
//从原有字符数组中分配缓冲区
char[] charArr = new char[100];
CharBuffer charBuffer2 = CharBuffer.wrap(charArr);
CharBuffer charBuffer2 = CharBuffer.wrap(charArr, 12, 43); //分配一部分
- 写入数据到缓冲区:
Channel.read(Buffer)/Buffer.put()
- 从
Channel中读数据到Buffer
int bytesRead = inChannel.read(buffer);
- 通过
put写数据
buffer.put(127);
- 从缓冲区读取数据:
Buffer.get() - 读写反转:调用
Buffer.flip()可以让limit = position; position = 0,从读模式切换到写模式 - 查询剩余元素:
Buffer.hasRemaining(),如果buffer非空,返回true - 清空
buffer:Buffer.clear()
等价于:
for(int i = 0; buffer.hasRemaining(), i++){
buffer.get();
}
2.通道(Channel)
FileChannelSocketChannelServerSocketChannelDatagramChannel
RandomAccessFile raf = new RandomAccessFile("file", "r");
FileChannel fc = raf.getChannel();
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("host", port));
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(localport));
DatagramChannel dc = DatagramChannel.open();
//channel.read()
//channel.write()
//channel.close()
(1)多缓冲区操作
Scatter:将1个Channel中的数据分散到N个Buffer中去
public interface ScatteringByteChannel extends ReadableByteChannel{
public long read(ByteBuffer[] dsts) throws IOException;
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
}
Gather:将N个Buffer中的数据按照顺序发送到一个Channel
public interface GatheringByteChannel extends ReadableByteChannel{
public long write(ByteBuffer[] srcs) throws IOException;
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
}
(2)多通道之间数据传输
transferFrom():把数据从通道源传输到FileChanneltransferTo():将FileChannel数据传输到其他通道
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// There are more other methods
public abstract long transferTo (long position, long count, WritableByteChannel target);
public abstract long transferFrom (ReadableByteChannel src, long position, long count);
}
3.选择器/多路复用器(Selector)
通过单线程的Selector检查多个Channel是否处于可读/可写,从而实现单线程管理多个通道。
(1)优点
Selector通过多路复用避免了多线程,从而减少了线程上下文切换的开销。
(2)使用选择器
- 创建:
Selector.open()
Selector selector = Selector.open();
- 注册
Channel到Selector:Channel.register(...)
channel.configureBlocking(false); //通道必须是非阻塞的
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
//一个SelectionKey表示了一个Selector和一个Channel之间的绑定关系
demo
public static void main(String[] args) throws IOException{
//NIO
System.out.println("============NIO============");
RandomAccessFile file = new RandomAccessFile("C:\\Users\\19047535\\IdeaProjects\\batal\\src\\io\\text.txt", "r");
//通过文件打开channel
FileChannel channel = file.getChannel();
//分配buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) > 0){
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
System.out.println((char)buffer.get());
}
buffer.clear();
}
channel.close();
file.close();
}
内存映射文件
内存映射文件(memory-mapped file)能让你创建/修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读入内存,然后把它当作一个非常大的数组来访问。
内存映射文件虽然最后还是需要从磁盘读取文件,但是它不需要将数据读取到OS的内核缓冲区,而是直接将进程的用户私有空间和文件磁盘地址进行映射,就好像是从内存中读/写文件一样,所以才快。
(三)AIO
异步非阻塞I/O:应用操作之后直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO包括Sockets和Files两部分的异步通道接口及实现,并尽量使用OS提供的原生本地I/O功能进行实现。
AIO 和 NIO 的区别
NIO是OS有了I/O资源后,通知调用方(I/O线程)执行I/O操作;AIO是直接将I/O操作以函数的形式传给OS,有资源的时候由OS代替执行,只以Future或Callback的形式返回一个异步的结果给调用方(I/O线程)。
AIO的I/O操作
1.Future
提交一个I/O操作请求,返回一个Future。然后调用方可以通过Future.get()进行检查,确认它是否完成,或者阻塞I/O操作直到正常完成后超时异常。
//异步通道
AsynchronousSocketChannel ch = AsynchronousSocketChannel.open();
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
//异步 读
Future<Integer> result = ch.read(buffer);
try{
//通过Future.get()异步获取执行结果
int bytesRead = result.get();
//Success
if(bytesRead == -1) return;
}catch(ExecutionException e) {
//Fail
}
注意:Future.get()是同步的,谨慎使用。
2.Callback
提交一个I/O操作请求,并且指定一个CompletionHandler。当异步I/O完成后,发送一个请求,此时CompletionHandler对象的completed/failed方法将会被回调。
public interface CompletionHandler<V, A> {
//当操作完成后被调用
//V result:操作结果
//A attachment:提交操作请求时的参数
void completed(V value, A attachment);
//当操作失败后被调用
//Throwable exc:失败原因
void failed(Throwable exc, A attachment);
}
3.Future 和 Callback 的区别
Future是调用方主动异步查询操作成功/失败,然后调用方再根据操作结果实现不同逻辑Callback是调用方直接将成功(completed)/失败(failed)时对应的操作以函数的方式传递给操作方,操作成功/失败后,操作方异步回调对应的函数
AsynchronousFileChannel:异步文件通道
AsynchronousFileChannel使数据可以异步读写。
