Java IO的演进

      一个系统的性能遵循木桶原理,IO的性能的高低直接影响着一个系统的吞吐量,尤其在高并发的场景下,IO的优化显的尤其重要。

1、名词概念

1.1、阻塞(Block)和非阻塞(Non-Block)

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪时的一种处理方式:
  • 阻塞:当数据没有准备的时候,往往需要等待缓冲区中的数据准备好过后才能继续处理,否则线程任务暂时放弃cpu,进入等待队列一直等待数据准备完成;
  • 非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备直接返回,不会等待;数据已经准备好,则直接返回;

1.2、同步(Sync)和异步(Async)

都是基于应用程序和操作系统处理IO事件所采用的方式。
  • 同步:应用程序线程直接参与IO读写操作的全过程;
  • 异步:所有的IO读写交给操作系统去处理,应用程序无需死等,只需被动等待处理结果的通知,期间可以做其他的任务。异步一般采用事件通知和回调的方式实现,此种方法一般使用较多;
    同步方式在处理IO事件的时候,必须阻塞在某个方法上面等待我们的IO事件完成(阻塞IO事件或者通过轮询IO事件的方式);
   对于 异步方式来说,所有的IO读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的IO操作,当操作完成IO后,操作系统会给我们的应用程序一个通知。
同步阻塞:线程将会阻塞到IO事件,例如accept()/read() 或者write(),这个时候线程完全不能做自己的事情,对线程的性能开销比较大。

1.3、B(Block)-IO和NIO

  • BIO:面向流操作,是一种阻塞IO。意味着,当一个线程调用accept()/read() 或 write()时,该线程将会被一直阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情了。
  • NIO:传统IO基于字节流和字符流进行操作,只能单向读写数据,是一种阻塞io。而Java NIO采用多路复用技术,借助于操作系统,基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中;Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达),实现了单个线程可以监听多个数据通道,提高了效率。

2、IO模型的演进过程

2.1、传统IO通信问题

(1). 阻塞IO使系统不能承受高并发请求;
    传统的RPC框架或者基于RMI等方式的远程服务(过程)调用采用了同步阻塞IO,当客户端的并发压力或者网络时延增大之后,同步阻塞IO会由于频繁的wait导致IO线程经常性的阻塞,由于线程无法高效的工作,IO处理能力自然下降。
(2). 线程过多占用cpu及系统资源;
    采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的“一请求一应答”模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是JAVA虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。
(3).数据编解码序列化的性能;
    Java序列化存在如下几个问题:
  • 1) Java序列化机制是Java内部的一种对象编解码技术,无法跨语言使用。例如对于异构系统之间的对接,Java序列化后的码流需要能够通过其它语言反序列化成原始对象(副本),目前很难支持;
  • 2) 相比于其它开源的序列化框架,Java 序列化后的码流太大,占用带宽大。无论是网络传输还是持久化到磁盘,都会导致额外的资源占用;
  • 3) 序列化性能差(CPU资源占用高)。由于采用同步阻塞IO,这会导致每个TCP连接都占用1个线程,当IO读写阻塞导致线程无法及时释放时,会导致虚拟机无法创建新的线程。

2.2、影响性能的因素

  • (1). 传输方式: 用什么样的通道将数据发送给对方,BIO、NIO或者AIO,IO模型在很大程度上决定了框架的性能;
  • (2). 数据协议: 采用什么样的通信协议,HTTP或者内部私有协议dubbo,thrift等。协议的选择不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计的更加简单高效。
  • (3). 线程模型: 数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,例如Reactor线程模型的使用对性能的影响也非常大。

2.3、BIO单线程模型

    最原始的网络编程 思路就是服务器使用一个while循环,不断监听端口是否有新的套接字连接,如果有新的连接就使用主线程继续调用一个处理函数handler去处理.
实现代码如下:
public static void main(String[] args) throws IOException{
    byte[] input = new byte[1024];
    ServerSocket serverSocket = new ServerSocket(8080);
    while(true){
        Socket clientSocket = serverSocket.accept();//accept阻塞;
        clientSocket.getInputStream().read(input);  //read()阻塞;
        output = process(input);
        System.out.println(new String(output));
    }
}

分析:这种阻塞IO通讯模型的最大问题是并发度,效率太低;accept()/read()/write()都是阻塞的,如果第一个请求connection1没有处理完,那么后面connection2和connection3的请求只能被阻塞,甚至都无法连接,请求无法被处理,此时服务器的吞吐量低,硬件cpu也没有被充分利用;

2.4、BIO多线程模型

    由于单线程的并发度太低,所以为了充分利用cpu,由于java对线程的良好支持,自然就会想到使用多线程,也就是很经典的connection-per-thread,每一个连接请求使用一个线程来处理,这样服务端在处理完客户端连接请求connection1以后就能马上返回处理下一个请求connection2,避免了读写数据的io阻塞使的后续请求无法被处理,提高了响应速度。
具体代码实现如下:
class ClassicServerModel implements Runnable {
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            while (!Thread.interrupted()){
            //创建新线程来handle请求,或者创建一个线程池来处理,但是线程用完以后还是会阻塞后面的请求;
                new Thread(new Handler(serverSocket.accept())).start();
            }
        } catch (IOException ex) { /* ... */ }
    }

    // 事件处理器handler
    static class Handler implements Runnable {
        final Socket socket;

        Handler(Socket s) {
            socket = s;
        }
        public void run() {
            try {
                byte[] input = new byte[1024];
                socket.getInputStream().read(input);
                byte[] output = process(input);
                socket.getOutputStream().write(output);
            } catch (IOException ex) {  }
        }
        private byte[] process(byte[] input) {
            byte[] output = null;
            //todo with input
            return output;
        }
    }
}

分析:优点:

  • (1). 一定程度上极大地提高了服务器的吞吐量,因为当请求在read()或者write()阻塞以后,不会影响到后续的socket请求,因为它们在不同的线程中,一个线程对应一个socket;
  • (2). 每个处理器handler拥有它自己的一个处理线程;
缺点:
  • (1). 一个连接创建一个线程导致数量过多:对于长连接的服务,有的请求仅仅只是一个空连接并没有数据的读取需求;
  • (2). 消耗 系统资源:创建线程过多,内存及cpu的上下文切换,缓存实效等等,而且,线程的创建-销毁也需要代价;
  • (3). 多线程的操作可能涉及到数据的线程安全,需要考虑同步控制;
问题思 考一: 如果一个线程中对应多个socket连接不行吗?
    编码实现确实可以,但是实际上没有用。因为每一个socket都是阻塞的,实际上还是单线在处理,一次只能处理一个socket,就算accept()了多个客户端连接,前一个socket被阻塞了,后面的请求依然无法处理执行,没有解决根本的问题;
问题思考二: 这里采用线程池为啥还是不行?
   注意:这里使用线程池,实际上没有从根本上解决io阻塞的问题。因为假设线程池的最大线程数量为10个,每个线程的任务阻塞io时间较长,这10个线程都用完了,后面的来的请求依然会被阻塞住,无法执行,对于一般的请求量OK,但对于高并发还是有性能瓶颈。

2.5、Java的NIO

    因为传统BIO阻塞的原因严重影响系统的性能,java使用了非阻塞的NIO来处理客户端的请求。 Java NIO采用多路复用技术,借助于操作系统事件的支持,使得单线程来监视处理多个连接请求成为了可能。 通过轮询“选择器-Selector找到准备就绪的通道进行处理,实现了非阻塞。当一个线程从某通道发送请求读取数据时,如果目前数据没有准备就绪,不会进行线程阻塞, 直至数据准备就绪读取之前,该线程可以返回继续做其他的事情,这样就实现了一个线程可以处理多个请求,充分利用cpu,提高效率, NIO的核心实现组件:
Channels: Connections to files, sockets etc that support non-blocking reads;
Buffers: Array-like objects that can be directly read or written by Channels; 
Selectors: Tell which of a set of Channels have IO events;
SelectionKeys:  Maintain IO event status and bindings。
   (1). 通道Channel:支持非阻塞的io读写,一个线程可以监听多个serverSocket,包括文件或者套接字socket;
   (2). 缓存buffer:类似于数组可以直接进行数据的读写操作;read()/write();
   (3). 选择器Selectors:检测发现那个通道集合里有准备就绪的io事件;
   (4). 事件属性SelectionKeys:维护io事件和channel关系的绑定;

实现一个线程处理多请求的NIO的具体流程如下图:

具体代码实例如下:
public class NIOServer {

    public static void nioServerDemo() throws IOException {
        // 1、获取Selector选择器
        Selector selector = Selector.open();
        // 2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4、绑定连接
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 5、将通道注册到选择器上,并注册的操作为:“接收”操作
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 6、单线程采用轮询的方式,查询获取“准备就绪”的注册过的操作;
        while (selector.select() > 0) {
            // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
            Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) {
                // 8、获取“准备就绪”的时间
                SelectionKey selectedKey = selectedKeys.next();
                // 9、判断key是具体的什么事件,然后dispatcher给不同的handler处理;
                if (selectedKey.isAcceptable()) {
                    // 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 11、设置socker为非阻塞模式;
                    socketChannel.configureBlocking(false);
                    // 12、将该通道注册到selector选择器上;
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectedKey.isReadable()) {
                    // 13、获取该选择器上的“读就绪”状态的通道;
                    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
                    // 14、读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    while ((length = socketChannel.read(byteBuffer)) != -1) {
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(), 0, length));
                        byteBuffer.clear();
                    }
                    socketChannel.close();
                }
                // 15、移除选择键
                selectedKeys.remove();
            }
        }
        // 16、关闭连接
        serverSocketChannel.close();
    }

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

分析:通过操作系统事件机制的支持,每次轮询Selector集合中感兴趣的事件,返回的事件都是数据已经准备就绪的socket请求,线程直接处理准备就绪的请求,而不用阻塞在接受连接/读/写数据的过程中,实现了线程的非阻塞,大大提高了cpu的利用率,这是实现非阻塞通信的基础。

3、Reactor模式

    基于java的NIO网络通信,Reactor模式提供了一种更加高性能的网络通信方式,也称为反应器模式。此种模式通过良好的模块功能设计,接受连接acceptor,事件类型任务的分配dispatcher,任务的处理handler等各司其职,充分的利用了事件通知驱动以及线程异步处理的机制,大大提高了系统的并发度和吞吐量。由于其高性能,大多数基础组建都在使用,例如Netty,zk,dubbo,redis等。

3.1、单Reactor单线程模型

Reactor模式,主要基于事件驱动(event handling),主要是基于Java NIO的实现封装,它使的一个线程可以管理过个客户端的连接。在NIO的基础上,Reactor主要抽象出Reactor和Handler两个组件:
   (1). Reactor:负责响应IO事件,当检测到一个新的事件,将其Dispatcher发送给相应的Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。
   (2). Handler: 将自身(handler)与事件绑定,负责具体事件类型的处理,完成channel的读入,执行完业务逻辑后,负责将结果写出channel。Accpet()也可以看成是一种特殊的handler;
Reactor对请求事件的处理过程:
  • (1). 循环检测处理客户端的新请求,将请求放入队列,accept();
  • (2). 同步轮询检测事件的准备就绪情况,使用Selector实现;
  • (3). 服务端同意按不同的事件类型分配给相应的事件处理器处理,dispatch完成任务分派;
  • (4). 将事件的处理器和dispatch事件分派服务解耦,单一职责事件处理-handler;
线程的处理过程如下图所示:
具体代码实现:
class SingleThreadReactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocket;

    //Reactor初始化
    SingleThreadReactor(int port) throws IOException {
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        //非阻塞
        serverSocket.configureBlocking(false);

        //分步处理,第一步,接收accept事件
        SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        //attach Acceptor 处理新连接
        sk.attach(new Acceptor());
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while (it.hasNext()) {
                    // Reactor负责dispatch收到的事件
                    // 若是连接事件获取是acceptor
                    // 若是IO读写事件获取是handler
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException ex) { 
            e.printStack();
        }
    }
    //根据事件类型分派任务处理--Reactor线程
    void dispatch(SelectionKey k) {
        Runnable r = (Runnable) (k.attachment());
        //调用之前注册的callback对象
        if (r != null) {
            r.run();
        }
    }

    // 处理新连接请求accept()--Reactor线程
    class Acceptor implements Runnable {
        public void run() {
            try {
                SocketChannel channel = serverSocket.accept();
                if (channel != null)
                    new Handler(selector, channel);
            } catch (IOException ex) { 
                e.printStack();
            }
        }
    }
}

//不同事件类型的处理器----Reactor线程
class Handler implements Runnable {
    final SocketChannel channel;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(1024);
    ByteBuffer output = ByteBuffer.allocate(1024);

    Handler(Selector selector, SocketChannel socketChannel) throws IOException {
        channel = socketChannel;
        socketChannel.configureBlocking(false);
        // Optionally try first read now
        sk = channel.register(selector, SelectionKey.OP_READ);

        //将Handler作为callback对象
        sk.attach(this);

        //第二步,注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override
    public void run() {
        if(sc.isOpen() && sk.isValid()){
            if(sk.isReadable()){
                doRead();//处理读就绪事件handler
            }else if(sk.isWritable()){
                doSend();//处理写就绪事件handler
            }
        }else{
            LOG.error("try to do read/write operation on null socket");
            try {
                if(sc != null)
                    sc.close();
            } catch (IOException e) {}
        }
}

单Reactor单线程模型缺点:

  • (1). 在单线程方式中,dispatch方法是同步阻塞的,所有的IO操作和业务逻辑处理都是主线程-Reactor线程中完成,如果业务处理很快,那么这种实现方式没什么问题。但是,如果业务处理很耗时(涉及很多数据库操作、磁盘操作等),那么这种情况下Reactor将被阻塞,当其中某个 handler阻塞时, 会导致其他所有的client的handler都得不到执行,并且更严重的是,handler的阻塞也会导致整个服务不能接收新的client请求,因为acceptor也被阻塞了。
  • (2). 单线程的可靠性低,单点故障;
    因此,单线程模型仅仅适用于handler中业务处理组件能快速完成的场景,这种场景还避免了数据同步同步读写的问题;
由于这种单线程模型不能充分利用多核资源,单线程的另外一个问题是在大负载的情况下,Reactor的处理速度必然会成为系统性能的瓶颈,所以实际使用的不多。
解决方法:
    业务逻辑进行异步处理,即交给用户线程池处理。

3.2、单Reactor多线程模型

由于单线程的性能瓶颈问题,在单Reactor单线程模式基础上,增加了业务处理线程池,出现了单Reactor多线程模型。
  • 将Handler处理器的执行放入worker线程池,线程异步进行业务处理,减少阻塞;
其工作流程如下:
改进:在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,这样可以减小主线程Reactor的性能开销,从而更专注的做事件分发dispatch工作了,从而提升整个应用的吞吐,这也是Netty的默认NIO模式。
单Reactor多线程模型的特点:
  • (1). 有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求;
  • (2). 网络IO操作读、写等由一个NIO业务线程池来负责,线程池包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;
  • (3). 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题,无锁化串性编程是NIO高性能通信的一个重要思想和设计。
handler的处理逻辑放入线程池,代码改进如下:
public class MultThreadHandler implements Runnable {

    final SocketChannel channel;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(1024);
    ByteBuffer output = ByteBuffer.allocate(1024);

    //创建业务处理线程池
    ExecutorService threadPool = new ThreadPoolExecutor(5, 10, 20, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(1000));

    MultThreadHandler(Selector selector, SocketChannel c) throws IOException {
        channel = c;
        c.configureBlocking(false);
        // Optionally try first read now
        sk = channel.register(selector, 0);

        //将Handler作为callback对象
        sk.attach(this);

        //第二步,注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    @Override//业务处理逻辑
    public void run() {
        try {  //根据事件的不同类型交由线程池处理
            if (channel.isOpen() && sk.isValid()) {
                if (sk.isReadable()) {
                    threadPool.submit(() -> doRead());//线程池处理读就绪事件handler
                } else if (sk.isWritable()) {
                    threadPool.submit(() -> doWrite());//线程池处理写就绪事件handler
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    synchronized void doRead() {
        // ...读操作
    }

    synchronized void doWrite() {
        // ...写操作
    }

3.3、Reactor主-从线程模型

    在绝大多数场景下,Reactor多线程模型都可以满足性能需求。但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,鉴权校验,这个过程比较损耗性能,在这类场景下,对于Reactor而言仍为单个线程,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,充分利用多核的CPU,可以将Reactor拆分为多线程,利用subReactor进行数据的处理和派发。因此产生了第三种多Reactor主-从多线程模型。
    在此模型中,Reactor将流量请求根据不同的事件类型读写,或者登陆鉴权,安全认证等细分为不同的subReactor集合,一个selector代表一个SubReactor。我们可以看到,mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,每个subReactor由一个线程来处理,具体的io操作使用workerGroup线程来异步处理。。此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。
其工作流程如下图:
将Reactor拆分为多SubReactor,一个selector就是一个SubReactor,代码实现方式:
//多Reactor线程:
public class MultReactor implements Runnable {
    
    //Reactor线程和cpu的数量一直,主要用于充分利用cpu;
    int workCount = Runtime.getRuntime().availableProcessors();
    SubReactor[] workThreadHandlers = new SubReactor[workCount];
    volatile int nextHandler = 0;

    public MultReactor() {
        this.init();
    }

    public void init() {
        nextHandler = 0;
        for (int i = 0; i < workThreadHandlers.length; i++) {
            try {
                workThreadHandlers[i] = new SubReactor();
            } catch (Exception e) {
            }
        }
    }

    @Override
    public void run() {
        try {
            SocketChannel c = serverSocket.accept();
            if (c != null) {// 注册读写
                synchronized (c) {
                    // 顺序获取SubReactor,然后注册channel和事件;
                    SubReactor work = workThreadHandlers[nextHandler];
                    work.registerChannel(c);
                    nextHandler++;
                    if (nextHandler >= workThreadHandlers.length) {
                        nextHandler = 0;//从头开始轮询,均匀的分配请求;
                    }
                }
            }
        } catch (Exception e) {
        }
    }
}
子subReactor中采用多work线程处理阻塞io任务:
//多work线程处理读写业务逻辑
class SubReactor implements Runnable {
    final Selector mySelector;

    //多线程处理业务逻辑
    int workCount = Runtime.getRuntime().availableProcessors();
    ExecutorService executorService = Executors.newFixedThreadPool(workCount);

    public SubReactor() throws Exception {
        // 每一个SubReactor就是一个selector
        this.mySelector = SelectorProvider.provider().openSelector();
    }

    /**
     * 注册channel
     */
    public void registerChannel(SocketChannel sc) throws Exception {
        sc.register(mySelector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT);
    }

    @Override
    public void run() {
        while (true) {
            try {
                //每个SubReactor完成自己的事件分派和处理读写事件,和Reactor多线程模型逻辑保持一致;
                mySelector.select();
                Set<SelectionKey> keys = mySelector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        read();
                    } else if (key.isWritable()) {
                        write();
                    }
                }
            } catch (Exception e) {

            }
        }
    }

    private void read() {
        //任务异步处理
        executorService.submit(() -> process());
    }

    private void write() {
        //任务异步处理
        executorService.submit(() -> process());
    }

    /**
     * task 业务处理
     */
    public void process() {
        //todo具体的业务处理逻辑;
    }
}

当然也不是说第三种模式就最好,还是要根据具体的实际业务情况来选择合适的模型。Reactor编程的优点:

  • (1). 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
  • (2). 使用Netty封装的NIO编程相对简单,无锁化串行编程设计最大程度的避免复杂的多线程及同步问题;
  • (3). 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
  • (4). 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;

4、小结

    此篇主要是简单分析回顾了io的发展改进,主要目的是如何减少io阻塞,这种异步-事件驱动的消息处理思想值得借鉴。例如Netty通信默认使用的Reactor多线程模型,可以通过在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,来实现支持上述三种Reactor线程模型。
 
 
 
OK---操千曲而后晓声,观千剑而后识器。
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
 
 
参考资料:
《Netty实战》
“Scalable IO in Java”: http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
 
 
 
 
 
 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值