Netty4源码分析-NioEventLoop实现的线程运行逻辑

转载 2015年07月07日 17:54:35

netty服务端启动源码分析-线程创建一文中已分析SingleThreadEventExecutor所持有的线程的运行逻辑由NioEventLoop实现,那么本文就着手分析NioEventLoop实现的线程运行逻辑:

 

Java代码  收藏代码
  1. // NioEventLoop  
  2. protected void run() {  
  3.         for (;;) {  
  4.             oldWakenUp = wakenUp.getAndSet(false);  
  5.             try {  
  6.                 if (hasTasks()) {  
  7.                     selectNow();  
  8.                 } else {  
  9.                     select();  
  10.                     if (wakenUp.get()) {  
  11.                         selector.wakeup();  
  12.                     }  
  13.                 }  
  14.   
  15.                 cancelledKeys = 0;  
  16.   
  17.                 final long ioStartTime = System.nanoTime();  
  18.                 needsToSelectAgain = false;  
  19.                 if (selectedKeys != null) {  
  20.                     processSelectedKeysOptimized(selectedKeys.flip());  
  21.                 } else {  
  22.                     processSelectedKeysPlain(selector.selectedKeys());  
  23.                 }  
  24.                 final long ioTime = System.nanoTime() - ioStartTime;  
  25.   
  26.                 final int ioRatio = this.ioRatio;  
  27.                 runAllTasks(ioTime * (100 - ioRatio) / ioRatio);  
  28.   
  29.                 if (isShuttingDown()) {  
  30.                     closeAll();  
  31.                     if (confirmShutdown()) {  
  32.                         break;  
  33.                     }  
  34.                 }  
  35.             } catch (Throwable t) {  
  36.                 logger.warn("Unexpected exception in the selector loop.", t);  
  37.   
  38.                 // Prevent possible consecutive immediate failures that lead to  
  39.                 // excessive CPU consumption.  
  40.                 try {  
  41.                     Thread.sleep(1000);  
  42.                 } catch (InterruptedException e) {  
  43.                     // Ignore.  
  44.                 }  
  45.             }  
  46.         }  
  47.     }  

 分析如下:

 

  1. ioEventLoop执行的任务分为两大类:IO任务和非IO任务。IO任务即selectionKey中ready的事件,譬如accept、connect、read、write等;非IO任务则为添加到taskQueue中的任务,譬如之前文章中分析到的register0、bind、channelActive等任务
  2. 两类任务的执行先后顺序为:IO任务->非IO任务。IO任务由processSelectedKeysOptimized(selectedKeys.flip())或processSelectedKeysPlain(selector.selectedKeys())触发;非IO任务由runAllTasks(ioTime * (100 - ioRatio) / ioRatio)触发
  3. 两类任务的执行时间比由变量ioRatio控制,譬如:ioRatio=50(该值为默认值),则表示允许非IO任务执行的时间与IO任务的执行时间相等
  4. 执行IO任务前,需要先进行select,以判断之前注册过的channel是否已经有感兴趣的事件ready
  5. 如果任务队列中存在非IO任务,则执行非阻塞的selectNow()方法
    Java代码  收藏代码
    1. // NioEventLoop  
    2.   void selectNow() throws IOException {  
    3.         try {  
    4.             selector.selectNow();  
    5.         } finally {  
    6.             // restore wakup state if needed  
    7.             if (wakenUp.get()) {  
    8.                 selector.wakeup();  
    9.             }  
    10.         }  
    11.     }  
     否则,执行阻塞的select()方法
    Java代码  收藏代码
    1. // NioEventLoop  
    2.    private void select() throws IOException {  
    3.         Selector selector = this.selector;  
    4.         try {  
    5.             int selectCnt = 0;  
    6.             long currentTimeNanos = System.nanoTime();  
    7.             long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);  
    8.             for (;;) {  
    9.                 long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;  
    10.                 if (timeoutMillis <= 0) {  
    11.                     if (selectCnt == 0) {  
    12.                         selector.selectNow();  
    13.                         selectCnt = 1;  
    14.                     }  
    15.                     break;  
    16.                 }  
    17.                 int selectedKeys = selector.select(timeoutMillis);  
    18.                 selectCnt ++;  
    19.                 if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {  
    20.                     // Selected something,  
    21.                     // waken up by user, or  
    22.                     // the task queue has a pending task.  
    23.                     break;  
    24.                 }  
    25.   
    26.                 if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&  
    27.                         selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {  
    28.                     // The selector returned prematurely many times in a row.  
    29.                     // Rebuild the selector to work around the problem.  
    30.                     logger.warn(  
    31.                             "Selector.select() returned prematurely {} times in a row; rebuilding selector.",  
    32.                             selectCnt);  
    33.                     rebuildSelector();  
    34.                     selector = this.selector;  
    35.                     // Select again to populate selectedKeys.  
    36.                     selector.selectNow();  
    37.                     selectCnt = 1;  
    38.                     break;  
    39.                 }  
    40.                 currentTimeNanos = System.nanoTime();  
    41.             }  
    42.             if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {  
    43.                 if (logger.isDebugEnabled()) {  
    44.                     logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);  
    45.                 }  
    46.             }  
    47.         } catch (CancelledKeyException e) {  
    48.             if (logger.isDebugEnabled()) {  
    49.                 logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);  
    50.             }  
    51.             // Harmless exception - log anyway  
    52.         }  
    53.     }  
     下面分析阻塞的select方法:
  • 首先执行delayNanos(currentTimeNanos):计算延迟任务队列中第一个任务的到期执行时间(即最晚还能延迟执行的时间).注意:(每个SingleThreadEventExecutor都持有一个延迟执行任务的优先队列:final Queue<ScheduledFutureTask<?>> delayedTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>()),在启动线程的时候会往队列中加入一个任务)。最终的结果近似为:1秒钟-(当前时间-delayedTask创建的时间)。如果队列中没有任何任务,则默认返回1秒钟。
Java代码  收藏代码
  1. //SingleThreadEventExecutor  
  2. protected long delayNanos(long currentTimeNanos) {  
  3.         ScheduledFutureTask<?> delayedTask = delayedTaskQueue.peek();  
  4.         if (delayedTask == null) {  
  5.             return SCHEDULE_PURGE_INTERVAL;  
  6.         }  
  7.   
  8.         return delayedTask.delayNanos(currentTimeNanos);  
  9. }  
  10.   
  11. //ScheduledFutureTask  
  12. public long delayNanos(long currentTimeNanos) {  
  13.         return Math.max(0, deadlineNanos() - (currentTimeNanos - START_TIME));  
  14.     }  
  15. public long deadlineNanos() {  
  16.         return deadlineNanos;  
  17.     }  
  • 如果当前时间已经超过到期执行时间后的500000纳秒(这个数字是如何定的?),则说明被延迟执行的任务不能再延迟了:如果在进入这个方法后还没有执行过selectNow方法(由标记selectCnt是否为0来判断),则先执行非阻塞的selectNow方法,然后立即返回;否则,立即返回                                                                          
  • 如果当前时间没有超过到期执行时间后的500000L纳秒,则说明被延迟执行的任务还可以再延迟,所以可以让select的阻塞时间长一点(说不定多出的这点时间就能select到一个ready的IO任务),故执行阻塞的selector.select(timeoutMillis)方法
  • 如果已经存在ready的selectionKey,或者该selector被唤醒,或者此时非IO任务队列加入了新的任务,则立即返回
  • 否则,本次执行selector.select(timeoutMillis)方法后的结果selectedKeys肯定为0,如果连续返回0的select次数还没有超过SELECTOR_AUTO_REBUILD_THRESHOLD(默认值为512),则继续下一次for循环。注意,根据以下算法:long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L。随着currentTimeNanos的增大,在进入第二次for循环时,正常情况下(即:在没有selectionKey已ready的情况下,selector.select(timeoutMillis)确实阻塞了timeoutMillis毫秒才返回0)计算出的timeoutMillis肯定小于0,计算如下:
       假设第一次和第二次进入for循环时的当前时间分currentTimeNanos1,currentTimeNanos2,由于在第一次循环中select阻塞了timeoutMillis1毫秒,所以currentTimeNanons2纳秒 > currentTimeNanos1纳秒+timeoutMillis1毫秒.     那么,第二次的timeoutMillis2 =  (selectDeadLineNanos – currentTimeNanos2 + 500000) / 1000000 <  (selectDeadLineNanos – (currentTimeNanos1+timeoutMillis1*1000000)+ 500000) / 1000000 =

timeoutMillis1- timeoutMillis1=0

     即:timeoutMillis2 < 0。因此第二次不会再进行select,直接跳出循环并返回

 

  • 否则,如果连续多次返回0,说明每次调用selector.select(timeoutMillis)后根本就没有阻塞timeoutMillis时间,而是立即就返回了,且结果为0. 这说明触发了epool cpu100%的bug(https://github.com/netty/netty/issues/327)。解决方案就是对selector重新rebuild
Java代码  收藏代码
  1. public void rebuildSelector() {  
  2.         if (!inEventLoop()) {  
  3.             execute(new Runnable() {  
  4.                 @Override  
  5.                 public void run() {  
  6.                     rebuildSelector();  
  7.                 }  
  8.             });  
  9.             return;  
  10.         }  
  11.         final Selector oldSelector = selector;  
  12.         final Selector newSelector;  
  13.         if (oldSelector == null) {  
  14.             return;  
  15.         }  
  16.         try {  
  17.             newSelector = openSelector();  
  18.         } catch (Exception e) {  
  19.             logger.warn("Failed to create a new Selector.", e);  
  20.             return;  
  21.         }  
  22.         // Register all channels to the new Selector.  
  23.         int nChannels = 0;  
  24.         for (;;) {  
  25.             try {  
  26.                 for (SelectionKey key: oldSelector.keys()) {  
  27.                     Object a = key.attachment();  
  28.                     try {  
  29.                         if (key.channel().keyFor(newSelector) != null) {  
  30.                             continue;  
  31.                         }  
  32.                         int interestOps = key.interestOps();  
  33.                         key.cancel();  
  34.                         key.channel().register(newSelector, interestOps, a);  
  35.                         nChannels ++;  
  36.                     } catch (Exception e) {  
  37.                         logger.warn("Failed to re-register a Channel to the new Selector.", e);  
  38.                         if (a instanceof AbstractNioChannel) {  
  39.                             AbstractNioChannel ch = (AbstractNioChannel) a;  
  40.                             ch.unsafe().close(ch.unsafe().voidPromise());  
  41.                         } else {  
  42.                             @SuppressWarnings("unchecked")  
  43.                             NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;  
  44.                             invokeChannelUnregistered(task, key, e);  
  45.                         }  
  46.                     }  
  47.                 }  
  48.             } catch (ConcurrentModificationException e) {  
  49.                 // Probably due to concurrent modification of the key set.  
  50.                 continue;  
  51.             }  
  52.   
  53.             break;  
  54.         }  
  55.   
  56.         selector = newSelector;  
  57.   
  58.         try {  
  59.             // time to close the old selector as everything else is registered to the new one  
  60.             oldSelector.close();  
  61.         } catch (Throwable t) {  
  62.             if (logger.isWarnEnabled()) {  
  63.                 logger.warn("Failed to close the old Selector.", t);  
  64.             }  
  65.         }  
  66.   
  67.         logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");  
  68.     }  
      Rebuild的本质:其实就是重新创建一个selector,然后将原来的那个selector中已注册的所有channel重新注册到新的selector中,并将老的selectionKey全部cancel掉,最后将的selector关闭。对selector进行rebuild之后,还需要重新调用selectNow方法,检查是否有已ready的selectionKey.

     6. 执行select()或者selectNow()后,如果已经有已ready的selectionKey,则开始执行IO操。processSelectedKeysOptimized和processSelectedKeysPlain的执行逻辑是很相似的
Java代码  收藏代码
  1. // NioEventLoop  
  2.   private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {  
  3.         for (int i = 0;; i ++) {  
  4.             final SelectionKey k = selectedKeys[i];  
  5.             if (k == null) {  
  6.                 break;  
  7.             }  
  8.   
  9.             final Object a = k.attachment();  
  10.   
  11.             if (a instanceof AbstractNioChannel) {  
  12.                 processSelectedKey(k, (AbstractNioChannel) a);  
  13.             } else {  
  14.                 @SuppressWarnings("unchecked")  
  15.                 NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;  
  16.                 processSelectedKey(k, task);  
  17.             }  
  18.   
  19.             if (needsToSelectAgain) {  
  20.                 selectAgain();  
  21.                 // Need to flip the optimized selectedKeys to get the right reference to the array  
  22.                 // and reset the index to -1 which will then set to 0 on the for loop  
  23.                 // to start over again.  
  24.                 //  
  25.                 // See https://github.com/netty/netty/issues/1523  
  26.                 selectedKeys = this.selectedKeys.flip();  
  27.                 i = -1;  
  28.             }  
  29.         }  
  30.     }  
 
Java代码  收藏代码
  1. private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {  
  2.         // check if the set is empty and if so just return to not create garbage by  
  3.         // creating a new Iterator every time even if there is nothing to process.  
  4.         // See https://github.com/netty/netty/issues/597  
  5.         if (selectedKeys.isEmpty()) {  
  6.             return;  
  7.         }  
  8.   
  9.         Iterator<SelectionKey> i = selectedKeys.iterator();  
  10.         for (;;) {  
  11.             final SelectionKey k = i.next();  
  12.             final Object a = k.attachment();  
  13.             i.remove();  
  14.   
  15.             if (a instanceof AbstractNioChannel) {  
  16.                 processSelectedKey(k, (AbstractNioChannel) a);  
  17.             } else {  
  18.                 @SuppressWarnings("unchecked")  
  19.                 NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;  
  20.                 processSelectedKey(k, task);  
  21.             }  
  22.   
  23.             if (!i.hasNext()) {  
  24.                 break;  
  25.             }  
  26.   
  27.             if (needsToSelectAgain) {  
  28.                 selectAgain();  
  29.                 selectedKeys = selector.selectedKeys();  
  30.   
  31.                 // Create the iterator again to avoid ConcurrentModificationException  
  32.                 if (selectedKeys.isEmpty()) {  
  33.                     break;  
  34.                 } else {  
  35.                     i = selectedKeys.iterator();  
  36.                 }  
  37.             }  
  38.         }  
  39.     }  
      此处仅分析processSelectedKeysOptimized方法,对于这两个方法的区别暂时放下,后续再分析吧。processSelectedKeysOptimized的执行逻辑基本上就是循环处理每个select出来的selectionKey,每个selectionKey的处理首先根据attachment的类型来进行分发处理发:如果类型为AbstractNioChannel,则执行一种逻辑;其他,则执行另外一种逻辑。此处,本文仅分析类型为AbstractNioChannel的处理逻辑,另一种逻辑的分析暂时放下,后续再分析。

   在判断attachment的类型前,首先需要弄清楚这个attatchment是何时关联到selectionKey上的?还记得socket一文中分析的register0任务吗? AbstractNioChannel类中有如下代码:

selectionKey = javaChannel().register(eventLoop().selector, 0, this); 

   此处将this(即AbstractNioChannel)作为attachment关联到selectionKey

       现在开始分析类型为AbstractNioChannel的处理逻辑,首先看processSelectedKey(k, (AbstractNioChannel) a)的实现:
Java代码  收藏代码
  1. //NioEventLoop  
  2. private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {  
  3.         final NioUnsafe unsafe = ch.unsafe();  
  4.         if (!k.isValid()) {  
  5.             // close the channel if the key is not valid anymore  
  6.             unsafe.close(unsafe.voidPromise());  
  7.             return;  
  8.         }  
  9.   
  10.         int readyOps = -1;  
  11.         try {  
  12.             readyOps = k.readyOps();  
  13.             if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {  
  14.                 unsafe.read();  
  15.                 if (!ch.isOpen()) {  
  16.                     // Connection already closed - no need to handle write.  
  17.                     return;  
  18.                 }  
  19.             }  
  20.             if ((readyOps & SelectionKey.OP_WRITE) != 0) {  
  21.                 processWritable(ch);  
  22.             }  
  23.             if ((readyOps & SelectionKey.OP_CONNECT) != 0) {  
  24.                 // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking  
  25.                 // See https://github.com/netty/netty/issues/924  
  26.                 int ops = k.interestOps();  
  27.                 ops &= ~SelectionKey.OP_CONNECT;  
  28.                 k.interestOps(ops);  
  29.   
  30.                 unsafe.finishConnect();  
  31.             }  
  32.         } catch (CancelledKeyException e) {  
  33.             if (readyOps != -1 && (readyOps & SelectionKey.OP_WRITE) != 0) {  
  34.                 unregisterWritableTasks(ch);  
  35.             }  
  36.             unsafe.close(unsafe.voidPromise());  
  37.         }  
  38.     }  
       终于见到熟悉nio处理代码了,它根据selecionKey的readyOps的值进行分发,下一篇文章将分析readyOps为accept时的处理逻辑。关于final NioUnsafe unsafe = ch.unsafe(),还记得socket一文中分析的:NioUnsafe由AbstractChannel的子类AbstractNioMessageChannel实例化,其类型为NioMessageUnsafe,它里面定义了read方法,即readyOps为accept的处理逻辑。
   7.  执行完io任务后,接着执行非IO任务:runAllTasks(ioTime * (100 - ioRatio) / ioRatio)
Java代码  收藏代码
  1. //NioEventLoop  
  2. protected boolean runAllTasks(long timeoutNanos) {  
  3.         fetchFromDelayedQueue();  
  4.         Runnable task = pollTask();  
  5.         if (task == null) {  
  6.             return false;  
  7.         }  
  8.         final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;  
  9.         long runTasks = 0;  
  10.         long lastExecutionTime;  
  11.         for (;;) {  
  12.             try {  
  13.                 task.run();  
  14.             } catch (Throwable t) {  
  15.                 logger.warn("A task raised an exception.", t);  
  16.             }  
  17.             runTasks ++;  
  18.             // Check timeout every 64 tasks because nanoTime() is relatively expensive.  
  19.             // XXX: Hard-coded value - will make it configurable if it is really a problem.  
  20.             if ((runTasks & 0x3F) == 0) {  
  21.                 lastExecutionTime = ScheduledFutureTask.nanoTime();  
  22.                 if (lastExecutionTime >= deadline) {  
  23.                     break;  
  24.                 }  
  25.             }  
  26.   
  27.             task = pollTask();  
  28.             if (task == null) {  
  29.                 lastExecutionTime = ScheduledFutureTask.nanoTime();  
  30.                 break;  
  31.             }  
  32.         }  
  33.   
  34.         this.lastExecutionTime = lastExecutionTime;  
  35.         return true;  
  36.     }  

 首先分析fetchFromDelayedQueue()方法,由父类SingleThreadEventExecutor实现

Java代码  收藏代码
  1. // SingleThreadEventExecutor  
  2. private void fetchFromDelayedQueue() {  
  3.         long nanoTime = 0L;  
  4.         for (;;) {  
  5.             ScheduledFutureTask<?> delayedTask = delayedTaskQueue.peek();  
  6.             if (delayedTask == null) {  
  7.                 break;  
  8.             }  
  9.   
  10.             if (nanoTime == 0L) {  
  11.                 nanoTime = ScheduledFutureTask.nanoTime();  
  12.             }  
  13.   
  14.             if (delayedTask.deadlineNanos() <= nanoTime) {  
  15.                 delayedTaskQueue.remove();  
  16.                 taskQueue.add(delayedTask);  
  17.             } else {  
  18.                 break;  
  19.             }  
  20.         }  
  21.     }  

       其功能是将延迟任务队列(delayedTaskQueue)中已经超过延迟执行时间的任务迁移到非IO任务队列(taskQueue)中.然后依次从taskQueue取出任务执行,每执行64个任务,就进行耗时检查,如果已执行时间超过预先设定的执行时间,则停止执行非IO任务,避免非IO任务太多,影响IO任务的执行

 

 

总结:NioEventLoop实现的线程执行逻辑做了以下事情

  1. 先后执行IO任务和非IO任务,两类任务的执行时间比由变量ioRatio控制,默认是非IO任务允许执行和IO任务相同的时间
  2. 如果taskQueue存在非IO任务,或者delayedTaskQueue存在已经超时的任务,则执行非阻塞的selectNow()方法,否则执行阻塞的select(time)方法
  3. 如果阻塞的select(time)方法立即返回0的次数超过某个值(默认为512次),说明触发了epoll的cpu 100% bug,通过对selector进行rebuild解决:即重新创建一个selector,然后将原来的selector中已注册的所有channel重新注册到新的selector中,并将老的selectionKey全部cancel掉,最后将老的selector关闭
  4. 如果select的结果不为0,则依次处理每个ready的selectionKey,根据readyOps的值,进行不同的分发处理,譬如accept、read、write、connect等
  5. 执行完IO任务后,再执行非IO任务,其中会将delayedTaskQueue已超时的任务加入到taskQueue中。每执行64个任务,就进行耗时检查,如果已执行时间超过通过ioRatio和之前执行IO任务的耗时计算出来的非IO任务预计执行时间,则停止执行剩下的非IO任务

相关文章推荐

Netty解惑

一、IO模型1. BIO (同步阻塞模型)特点:阻塞式,通过一个独立的Acceptor线程负责监听客户端的连接,然后为每一个连接都创建一个新的线程进行处理 缺点:客户端的并发访问和服务端的线程数是1...
  • jeffleo
  • jeffleo
  • 2017年05月11日 12:27
  • 271

Netty 4源码解析:请求处理

Netty 4源码解析:请求处理通过之前《Netty 4源码解析:服务端启动》的分析,我们知道在最前端“扛压力”的是NioEventLoop.run()方法。我们指定创建出的NioServerSock...
  • dc_726
  • dc_726
  • 2015年08月29日 08:14
  • 5800

netty源码分析(十七)Netty线程模型深度解读与架构设计原则

上次分析到:public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements Event...

【Netty源码解析】NioEventLoop

上一篇博客【Netty源码学习】EventLoopGroup中我们介绍了EventLoopGroup,实际说来EventLoopGroup是EventLoop的一个集合,EventLoop是一个单线程...

Netty4学习笔记(6)-- NioEventLoop继承层次结构

分析NioEventLoop继承层次结构

剖析Netty4的高性能

1、Netty简介                       Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的...

【Netty源码解析】NioEventLoop

上一篇博客【Netty源码学习】EventLoopGroup中我们介绍了EventLoopGroup,实际说来EventLoopGroup是EventLoop的一个集合,EventLoop是一个单线程...

Netty5源码分析(二) -- 线程模型分析

线程模型是Netty的核心设计,设计地很巧妙,之前项目中有一块处理并发的设计和Netty的Eventloop单线程设计类似,效果得到了实证。 Netty5的类层次结构和之前的版本变化很大,网上也有...
  • ITer_ZC
  • ITer_ZC
  • 2014年09月18日 18:11
  • 4042

【Netty4.X】Netty源码分析之NioEventLoop(六)

继承关系: NioEventLoop初始化NioEventLoop(NioEventLoopGroup parent, ThreadFactory threadFactory, SelectorP...

Netty4 服务端启动源码分析-线程的创建

原文链接:http://xw-z1985.iteye.com/blog/1925013 本文分析Netty中boss和worker的线程的创建过程: 以下代码是服务端的启动代码,线程的...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Netty4源码分析-NioEventLoop实现的线程运行逻辑
举报原因:
原因补充:

(最多只允许输入30个字)