模型对比

image.png

bio 线程模型

image.png

伪异步 bio 线程模型,相对上面,只是每次有连接进来,交由线程池处理,减少了线程创建的开销。但 io 本质上还是阻塞的,所以把能够处理的请求有限。

nio 是基于 selector 或者 epoll 这样的系统调用函数,一个函数管理很多连接,每当有读写、建立连接事件,才执行。

BIO NIO代码实现

BIO

java 1.4 之前 只有 bio, api 基于 stream,byte,socket,serverSocket;

bio Socket Server demo:

  1. public class BioTest {
  2. private static final Executor executor = Executors.newFixedThreadPool(100);
  3. public static void main(String[] args) {
  4. startServer();
  5. }
  6. static void startServer() {
  7. try {
  8. ServerSocket serverSocket = new ServerSocket();
  9. serverSocket.bind(new InetSocketAddress(8080));
  10. while (true) {
  11. // 监听 op_accept
  12. Socket socket = serverSocket.accept();
  13. executor.execute(new IoHandler(socket));
  14. }
  15. } catch (Exception ex) {
  16. //ignore
  17. }
  18. }
  19. static class IoHandler implements Runnable {
  20. IoHandler(Socket socket) {
  21. this.socket = socket;
  22. }
  23. private Socket socket;
  24. @Override
  25. public void run() {
  26. while (!socket.isClosed()) {
  27. try {
  28. // 阻塞 read
  29. byte[] buffer = new byte[1024];
  30. InputStream ips = socket.getInputStream();
  31. int length;
  32. while ((length = ips.read(buffer)) != -1) {
  33. System.out.println(new String(buffer, 0, length));
  34. }
  35. // 阻塞 write
  36. socket.getOutputStream().write("hello server".getBytes());
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }
  42. }
  43. }

因为 socket.read(); socket.write()方法是同步阻塞的。所以 一个连接在服务端必须对应一个 thread 去处理;

bio 模型存在的问题:

  1. 该模型严重依赖与线程,java 线程创建、销毁会耗费大量的系统资源;
  2. 线程本身占用大量的系统资源,java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3. 每个线程中的 socket 的读写是阻塞的,意味着频繁的线程切换。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态;
  4. 可伸缩性差。上面的线程模型 单机连接 < 1000 的情况下 ok 的。 面对十万甚至百万级连接的时候,上面的BIO模型是无能为力的;

NIO

jdk 1.4 提供了一套新的api ,使 nio 编程成为可能。bio api基于 Stream,nio 基于 Bytebuffer;数据写入 bytebuffer ,基于 channel 进行传输;

byteBuffer 指针;
image.png

public abstract class Buffer {
  //标记读取or写入位置
  private int mark = -1;
  //已读已写的位置
  private int position = 0;
  //最大极限
  private int limit;
  //容器容量
  private int capacity;
  //重设位置
  public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
      throw new IllegalArgumentException();
    position = newPosition;
    //标记位超过新位置,重置为-1
    if (mark > position) mark = -1;
    return this;
  }
  //与position(int)方法同理
  public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
      throw new IllegalArgumentException();
    limit = newLimit;
    //如果位置超出新限制,则重合pos和limit
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
  }
}

byteBuffer 的分配可以在堆,也可以在直接内存,对比:

                |                                                  |                             <br /><br />DirectByteBuffer<br />DirectByteBufferdirect


                     | <br /><br />HeapByteBuffer
                 |

| :—-: | :—-: | :—-: | |

创建开销

                     |                             

                     |                             <br /><br /><br />小




                 |

|


存储位置

                     |                             

Native heap

                     |                             

Java heap

                 |

|

数据拷贝

                     |                             

无需临时缓冲区做拷贝

                     |                             <br /><br /><br />拷贝到临时

DirectByteBuffer,但临 时缓冲区使用缓存。 聚集写/发散读时 没有缓存临时缓冲区。

                 |

|

GC影响

                     |                             <br /><br /><br />每次创建或者释放的时候

都调用一次System.gc()

                     |                         
                 |

nio socket server demo:

 public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 1111);

        serverSocketChannel.bind(inetSocketAddress);

        serverSocketChannel.configureBlocking(false);

        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, null);

        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey myKey = iterator.next();

                if (myKey.isAcceptable()) {
                    // 连接建立(可以单独线程做)
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (myKey.isReadable()) {
                    // 处理read 事件,数据从 kernel->拷贝到用户空间 
                    // 单纯的io 读写,可以在单独的一个线程池里处理
                    SocketChannel socketChannel = (SocketChannel) myKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(256);
                    socketChannel.read(byteBuffer);

                   // 业务处理,可以在单独的线程池处理 
                   doBusiness(byteBuffer);
                }
                iterator.remove();
            }
        }
    }

基于 selector ,一个selector 可以管理 n 个socket连接。select 是事件驱动的,理论上一个线程可以完成所有io操作(accept,read,write)。相对于 bio ,节省了线程数量。 java 可以做的根据不同业务需求实现不同的线程模型。

select 的存在相当于 一个同步器,对感兴趣的事件进行下发,是 reactor 事件驱动模型的一种实现;

The reactor design pattern) is an event handling pattern for handling service requests delivered concurrently) to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.[1]

Boss thread + worker thread
Boss 处理 OP_ACCEPT、OP_CONNECT,处理连接接入
Worker 处理 OP_READ、OP_WRITE,处理IO读写
Reactor 线程数目 Netty 1+2*cpu

NIO 带来了什么?

  • 事件驱动模型

    避免多线程
    单线程处理多任务

  • 非阻塞IO,IO读写不再阻塞,而是返回0

  • 基于block的传输,通常比基于流的传输更高效

  • 更高级的IO函数,zero-copy

  • IO多路复用大大提高了java网络应用的可伸缩性和实用性

nio,bio 底层调用不同的 linux 系统函数;

socket-programming.png

nio 基于 selector 函数

305504-20150918012828961-1176245587.png

参考: https://tech.meituan.com/2016/11/04/nio.html

http://huangzehong.me/2019/01/09/20190109-%E3%80%90%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E3%80%91NIO%E7%9A%84%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90/

https://www.ibm.com/support/knowledgecenter/en/SSYKE2_8.0.0/com.ibm.java.80.doc/diag/problem_determination/aix_mem_heaps.html