同步,异步,阻塞,非阻塞
同步,异步:被调用方行为
阻塞,非阻塞:调用方行为
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步(事件,回调通知): 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
- 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
例子:
-
阻塞VS非阻塞:人是否坐在水壶前面一直等。
-
同步VS异步:水壶是不是在水烧开之后主动通知人。
组合方式:同步阻塞,同步非阻塞,异步非阻塞
Linux的5种IO模型
- 阻塞IO模型
- 非阻塞IO模型
- IO复用模型
- 信号驱动IO模型
- 异步IO模型
-
阻塞IO(例子+原理+场景)[同步阻塞]
-
我们钓鱼的时候,有一种方式比较惬意,比较轻松,那就是我们坐在鱼竿面前,这个过程中我们什么也不做,双手一直把着鱼竿,就静静的等着鱼儿咬钩。一旦手上感受到鱼的力道,就把鱼钓起来放入鱼篓中。然后再钓下一条鱼。
映射到Linux操作系统中,这就是一种最简单的IO模型,即阻塞IO。 阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。
-
应用进程通过系统调用
recvfrom
接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,直到内核准备好数据报,recvfrom
完成数据报复制工作,应用进程才能结束阻塞状态。 -
这种钓鱼方式相对来说比较简单,对于钓鱼的人来说,不需要什么特制的鱼竿,拿一根够长的木棍就可以悠闲的开始钓鱼了(实现简单)。缺点就是比较耗费时间,比较适合那种对鱼的需求量小的情况(并发低,时效性要求低)。
-
-
非阻塞IO(例子+原理+场景)[轮询][同步]
-
我们钓鱼的时候,在等待鱼儿咬钩的过程中,我们可以做点别的事情,比如玩一把王者荣耀、看一集《延禧攻略》等等。但是,我们要时不时的去看一下鱼竿,一旦发现有鱼儿上钩了,就把鱼钓上来。
映射到Linux操作系统中,这就是非阻塞的IO模型。应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。
-
应用进程通过
recvfrom
调用不停的去和内核交互,直到内核准备好数据。如果没有准备好,内核会返回error
,应用进程在得到error
后,过一段时间再发送recvfrom
请求。在两次发送请求的时间段,进程可以先做别的事情。 -
这种方式钓鱼,和阻塞IO比,所使用的工具没有什么变化,但是钓鱼的时候可以做些其他事情,增加时间的利用率。
-
-
信号驱动IO模型(例子+原理+场景)[回调][同步]
- 我们钓鱼的时候,为了避免自己一遍一遍的去查看鱼竿,我们可以给鱼竿安装一个报警器。当有鱼儿咬钩的时候立刻报警。然后我们再收到报警后,去把鱼钓起来。映射到Linux操作系统中,这就是信号驱动IO。应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。
- 应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中。
- 这种方式钓鱼,和前几种相比,所使用的工具有了一些变化,需要有一些定制(实现复杂)。但是钓鱼的人就可以在鱼儿咬钩之前彻底做别的事儿去了。等着报警器响就行了。
-
IO复用模型(例子+原理+场景)[多进程注册到一个管道,非阻塞][同步]
-
我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。
-
映射到Linux操作系统中,这就是IO复用模型。多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
-
IO多路转接是多了一个
select
函数,多个进程的IO可以注册到同一个select
上,当用户进程调用该select
,select
会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好时,select
调用进程会阻塞。当任意一个IO所需的数据准备好之后,select
调用就会返回,然后进程在通过recvfrom
来进行数据拷贝。这里的IO复用模型,并没有向内核注册信号处理函数,所以,他并不是非阻塞的。进程在发出
select
后,要等到select
监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝。 -
这种方式的钓鱼,通过增加鱼竿的方式,可以有效的提升效率。
-
-
异步IO [异步]
- 我们钓鱼的时候,采用一种高科技钓鱼竿,即全自动钓鱼竿。可以自动感应鱼上钩,自动收竿,更厉害的可以自动把鱼放进鱼篓里。然后,通知我们鱼已经钓到了,他就继续去钓下一条鱼去了。
- 映射到Linux操作系统中,这就是异步IO模型。应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。
- 用户进程发起
aio_read
操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到aio_read
后,会立刻返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户控件,然后再通知进程本次IO已经完成。 - 这种方式的钓鱼,无疑是最省事儿的。啥都不需要管,只需要交给鱼竿就可以了。
java中的三种IO模型
java中的IO还是借助操作系统的IO模型,只是对操作系统的IO模型进行了封装而已
BIO:同步阻塞,数据的读取写入必须阻塞在一个线程内等待其完成
- 它其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。
- 接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应
- 在响应返回前,客户端那边就阻塞等待,上门事情也做不了。
- 这种方式的缺点:每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端,这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。
NIO:支持同步阻塞和非阻塞,主要使用同步非阻塞,一个线程不断轮询被调用方的状态看看是否完成,从而进行下一步操作
- 其实相当于就是一个线程处理大量的客户端的请求,通过一个线程(selector)不停轮询大量的channel(socket连接=selectionKey.channel()),每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。
- 这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。
AIO:异步非阻塞,为每个被调用方增加一个回调,完成之后通知调用方
适应场景
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
代码+流程图
BIO实现
//server
// 监听8080端口
ServerSocket server = new ServerSocket(8080);
// 等待请求
Socket socket = server.accept();
// 进行通信
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println(line);
writer.flush(); // 不要忘了这个
// 关闭资源
writer.close();
reader.close();
socket.close();
server.close();
//client
// 监听8080端口
Socket socket = new Socket("127.0.0.1", 8080);
// 开始通信
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println("hello");
writer.flush();
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
// 关闭资源
writer.close();
reader.close();
socket.close();
NIO实现
//服务端
// 选择器、字符集
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");
// 打开管道
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
server.socket().bind(isa);
server.configureBlocking(false);
// 打开选择器
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
// 依次处理选择器上的选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
selector.selectedKeys().remove(sk);
// 连接请求
if(sk.isAcceptable()) {
SocketChannel sc = server.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
sk.interestOps(SelectionKey.OP_ACCEPT);
}
// 存在可读取数据
if(sk.isReadable()) {
// 使用缓冲区读取
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
try {
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
System.out.println("=======" + content);
// 将管道设置为准备下一次读取
sk.interestOps(SelectionKey.OP_READ);
} catch(IOException e) {
// 如果该sk对应的管道出现异常,表明管道的客户端出现异常,
// 所以从选择器中取消sk
e.printStackTrace();
sk.cancel();
if(sk.channel() != null) {
sk.channel().close();
}
}
// 说明聊天信息不为空
if(content.length() > 0) {
// 将聊天信息输入每个选择键对应的管道中
for(SelectionKey key : selector.keys()) {
Channel targetChannel = key.channel();
if(targetChannel instanceof SocketChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
// 客户端
// 管道、选择器、字符集
private SocketChannel sc = null;
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");
//创建线程类用于从服务端获取数据
private class ClientThread extends Thread {
public void run() {
try {
// 遍历所有选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
// 删除正在处理的
selector.selectedKeys().remove(sk);
// 如果该键对应的通道中有可读的数据
if(sk.isReadable()) {
// 使用缓冲区读取管道内的数据
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
// 打印
System.out.println("聊天信息" + content);
sk.interestOps(SelectionKey.OP_READ);
}
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
// 初始化SocketChannel
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
sc = SocketChannel.open(isa);
sc.configureBlocking(false);
// 注册选择器
selector = Selector.open();
sc.register(selector, SelectionKey.OP_READ);
创建线程从服务端拉取数据,及不断从键盘读入发送到服务端
// 启动线程不断从服务端拉取
new ClientThread().start();
// 读取键盘输入到通道
Scanner reader = new Scanner(System.in);
while(reader.hasNextLine()) {
String line = reader.nextLine();
sc.write(charset.encode(line));
}
AIO实现文件的读取和写入
- 给NIO增加了一个回调机制CompletionHandler
public class ReadFromFile {
public static void main(String[] args) throws Exception {
Path file = Paths.get("/usr/a.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
ByteBuffer buffer = ByteBuffer.allocate(100_000);
Future<Integer> result = channel.read(buffer, 0);
while (!result.isDone()) {
ProfitCalculator.calculateTax();
}
Integer bytesRead = result.get();
System.out.println("Bytes read [" + bytesRead + "]");
}
}
class ProfitCalculator {
public ProfitCalculator() {
}
public static void calculateTax() {
}
}
public class WriteToFile {
public static void main(String[] args) throws Exception {
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("/asynchronous.txt"), StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
CompletionHandler<Integer, Object> handler = new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println("Attachment: " + attachment + " " + result
+ " bytes written");
System.out.println("CompletionHandler Thread ID: "
+ Thread.currentThread().getId());
}
@Override
public void failed(Throwable e, Object attachment) {
System.err.println("Attachment: " + attachment + " failed with:");
e.printStackTrace();
}
};
System.out.println("Main Thread ID: " + Thread.currentThread().getId());
fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write",
handler);
fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write",
handler);
}
}
java之NIO
-
IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO的各种流是阻塞的。这意味着,当一个线程调用
read()
或write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了 -
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
-
NIO 通过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
-
NIO有选择器Selector,而IO没有。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
reference:https://mp.weixin.qq.com/s/YIcXaH7AWLJbPjnTUwnlyQ