20 June 2020

同步,异步,阻塞,非阻塞

同步,异步:被调用方行为

阻塞,非阻塞:调用方行为

  • 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步(事件,回调通知): 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
  • 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

例子:

  • 阻塞VS非阻塞:人是否坐在水壶前面一直等。

  • 同步VS异步:水壶是不是在水烧开之后主动通知人。

组合方式:同步阻塞,同步非阻塞,异步非阻塞

Linux的5种IO模型

  • 阻塞IO模型
  • 非阻塞IO模型
  • IO复用模型
  • 信号驱动IO模型
  • 异步IO模型

企业微信截图_36c4a9ae-8515-4e92-8a93-9f6f8b0b9d8d.png

  • 阻塞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上,当用户进程调用该selectselect会监听所有注册好的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实现

WechatIMG29.png

//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实现

WechatIMG30.png

//服务端
// 选择器、字符集
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



blog comments powered by Disqus