NIO之多线程协作处理数据读写

开发 前端
单线程下的NIO存在性能瓶颈,当某一计算过程缓慢的时候会阻塞住整个线程,导致影响其他事件的处理!

[[407963]]

本文转载自微信公众号「源码学徒」,作者皇甫嗷嗷叫 。转载本文请联系源码学徒公众号。

经过前面几章的学习,我们已经 能够掌握了JDK NIO的开发方式,我们来总结一下NIO开发的流程:

  1. 创建一个服务端通道 ServerSocketChannel
  2. 创建一个选择器 Selector
  3. 将服务端通道注册到选择器上,并且关注我们感兴趣的事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  4. 绑定服务管道的地址 serverSocketChannel.bind(new InetSocketAddress(8989));
  5. 开始进行事件选择,选择我们感兴趣的事件做对应的操作!

具体的代码信息请参照第一章:多路复用模型章节,这里不做太多的赘述!

有关多路复用的概念,我们也在第一章进行了分析。多路复用模型能够最大限度的将一个线程的执行能力榨干,一条线程执行所有的数据,包括新连接的接入、数据的读取、计算与回写,但是假设,我们的数据计算及其缓慢,那么该任务的执行就势必影响下一个新链接的接入!

传统NIO单线程模型

单线程的NIO模型

如图,我们能了解到,单线程情况下,读事件因为要做一些业务性操作(数据库连接、图片、文件下载)等操作,导致线程阻塞再,读事件的处理上,此时单线程程序无法进行下一次新链接的处理!我们对该线程模型进行优化,select事件处理封装为任务,提交到线程池!

NIO多线程模型

上面的这种数据结构能够解决掉因为计算任务耗时过长,导致新链接接入阻塞的问题,我们能否再次进行一次优化呢?

我们能否创建多个事件选择器,每个事件选择器,负责不同的Socket连接,就像下面这种:

NIO多线程优化模型

这样我们就可以每一个Select选择器负责多个客户端Socket连接,主线程只需要将客户端新连接选择一个选择器注册到select选择器上就可以了!所以我们的架构图,就变成了下图这样:

我们在select选择器内部处理计算任务的时候,也可以将任务封装为task,提交到线程池里面去,彻底将新连接接入和读写事件处理分离开,互不影响!事实上,这也是Netty的核心思想之一,我们可以根据上面的图例,自己简单写一个:

代码实现

构建一个事件执行器 对应上图的select选择器

  1. /** 
  2.  * Nio事件处理器 
  3.  * 
  4.  * @author huangfu 
  5.  * @date 
  6.  */ 
  7. public class MyNioEventLoop implements Runnable { 
  8.     static final ByteBuffer ALLOCATE = ByteBuffer.allocate(128); 
  9.     private final Selector selector; 
  10.     private final LinkedBlockingQueue<Runnable> linkedBlockingQueue; 
  11.     public MyNioEventLoop(Selector selector) { 
  12.         this.selector = selector; 
  13.         linkedBlockingQueue = new LinkedBlockingQueue<>(); 
  14.     } 
  15.  
  16.     public Selector getSelector() { 
  17.         return selector; 
  18.     } 
  19.  
  20.     public LinkedBlockingQueue<Runnable> getLinkedBlockingQueue() { 
  21.         return linkedBlockingQueue; 
  22.     } 
  23.  
  24.     //忽略  hashCode和eques 
  25.  
  26.     /** 
  27.      * 任务处理器 
  28.      */ 
  29.     @Override 
  30.     public void run() { 
  31.         while (!Thread.currentThread().isInterrupted()) { 
  32.             try { 
  33.                 //进行事件选择  这里我们只处理读事件 
  34.                 if (selector.select() > 0) { 
  35.                     Set<SelectionKey> selectionKeys = selector.selectedKeys(); 
  36.                     Iterator<SelectionKey> iterator = selectionKeys.iterator(); 
  37.                     //处理读事件 
  38.                     while (iterator.hasNext()) { 
  39.                         SelectionKey next = iterator.next(); 
  40.                         iterator.remove(); 
  41.                         if (next.isReadable()) { 
  42.                             SocketChannel channel = (SocketChannel) next.channel(); 
  43.                             int read = channel.read(ALLOCATE); 
  44.                             if(read > 0) { 
  45.                                 System.out.printf("线程%s【%s】发来消-息:",Thread.currentThread().getName(), channel.getRemoteAddress()); 
  46.                                 System.out.println(new String(ALLOCATE.array(), StandardCharsets.UTF_8)); 
  47.                             }else if(read == -1) { 
  48.                                 System.out.println("连接断开"); 
  49.                                 channel.close(); 
  50.                             } 
  51.                             ALLOCATE.clear(); 
  52.                         } 
  53.                     } 
  54.                     selectionKeys.clear(); 
  55.                 }else { 
  56.                     //处理异步任务  进行注册 
  57.                     while (!linkedBlockingQueue.isEmpty()) { 
  58.                         Runnable take = linkedBlockingQueue.take(); 
  59.                         //异步事件执行 
  60.                         take.run(); 
  61.                     } 
  62.                 } 
  63.             } catch (IOException | InterruptedException e) { 
  64.                 e.printStackTrace(); 
  65.             } 
  66.         } 
  67.     } 

构建一个选择器组

  1. /** 
  2.  * 选择器组 
  3.  * 
  4.  * @author huangfu 
  5.  * @date 2021年3月12日09:44:37 
  6.  */ 
  7. public class SelectorGroup { 
  8.     private final List<MyNioEventLoop> SELECTOR_GROUP = new ArrayList<>(8); 
  9.     private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors(); 
  10.     private final static AtomicInteger IDX = new AtomicInteger(); 
  11.  
  12.     /** 
  13.      * 初始化选择器 
  14.      * @param count 处理器数量 
  15.      * @throws IOException 异常欣喜 
  16.      */ 
  17.     public SelectorGroup(int count) throws IOException { 
  18.  
  19.         for (int i = 0; i < count; i++) { 
  20.             Selector open = Selector.open(); 
  21.             MyNioEventLoop myNioEventLoop = new MyNioEventLoop(open); 
  22.             SELECTOR_GROUP.add(myNioEventLoop); 
  23.         } 
  24.     } 
  25.  
  26.     public SelectorGroup() throws IOException { 
  27.         this(AVAILABLE_PROCESSORS << 1); 
  28.     } 
  29.  
  30.     /** 
  31.      * 轮询获取一个选择器 
  32.      * @return 返回一个选择器 
  33.      */ 
  34.     public MyNioEventLoop next(){ 
  35.         int andIncrement = IDX.getAndIncrement(); 
  36.         int length = SELECTOR_GROUP.size(); 
  37.  
  38.         return SELECTOR_GROUP.get(Math.abs(andIncrement % length)); 
  39.     } 

构建一个执行器记录器

  1. /** 
  2.  * @author huangfu 
  3.  * @date 
  4.  */ 
  5. public class ThreadContext { 
  6.     /** 
  7.      * 记录当前使用过的选择器 
  8.      */ 
  9.     public static final Set<MyNioEventLoop> RUN_SELECT = new HashSet<>(); 

构建一个新连接接入选择器

  1. /** 
  2.  * 连接器 
  3.  * 
  4.  * @author huangfu 
  5.  * @date 2021年3月12日10:15:37 
  6.  */ 
  7. public class Acceptor implements Runnable { 
  8.     private final ServerSocketChannel serverSocketChannel; 
  9.     private final SelectorGroup selectorGroup; 
  10.  
  11.     public Acceptor(ServerSocketChannel serverSocketChannel, SelectorGroup selectorGroup) { 
  12.         this.serverSocketChannel = serverSocketChannel; 
  13.         this.selectorGroup = selectorGroup; 
  14.     } 
  15.  
  16.  
  17.     @Override 
  18.     public void run() { 
  19.         try { 
  20.             SocketChannel socketChannel = serverSocketChannel.accept(); 
  21.             MyNioEventLoop next = selectorGroup.next(); 
  22.  
  23.             //向队列追加一个注册任务 
  24.             next.getLinkedBlockingQueue().offer(() -> { 
  25.                 try { 
  26.                     //客户端注册为非阻塞 
  27.                     socketChannel.configureBlocking(false); 
  28.                     //注册到选择器 关注一个读事件 
  29.                     socketChannel.register(next.getSelector(), SelectionKey.OP_READ); 
  30.                 } catch (Exception e) { 
  31.                     e.printStackTrace(); 
  32.                 } 
  33.             }); 
  34.             //唤醒对应的任务,让其处理异步任务 
  35.             next.getSelector().wakeup(); 
  36.  
  37.  
  38.             System.out.println("检测到连接:" + socketChannel.getRemoteAddress()); 
  39.             //当当前选择器已经被使用过了  就不再使用了,直接注册就行了 
  40.             if (ThreadContext.RUN_SELECT.add(next)) { 
  41.                 //启动任务 
  42.                 new Thread(next).start(); 
  43.             } 
  44.  
  45.  
  46.         } catch (IOException e) { 
  47.             e.printStackTrace(); 
  48.         } 
  49.     } 

创建启动器

  1. /** 
  2.  * @author huangfu 
  3.  * @date 
  4.  */ 
  5. public class TestMain { 
  6.  
  7.     public static void main(String[] args) throws IOException { 
  8.         //创建一个选择器组   传递选择器组的大小 决定使用多少选择器来实现 
  9.         SelectorGroup selectorGroup = new SelectorGroup(2); 
  10.         //开启一个服务端管道 
  11.         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
  12.         //开启一个服务端专用的选择器 
  13.         Selector selector = Selector.open(); 
  14.         //设置非阻塞 
  15.         serverSocketChannel.configureBlocking(false); 
  16.         //创建一个连接器 
  17.         Acceptor acceptor = new Acceptor(serverSocketChannel, selectorGroup); 
  18.         //将服务端通道注册到服务端选择器上  这里会绑定一个新连接接入器 
  19.         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, acceptor); 
  20.         //绑定端口 
  21.         serverSocketChannel.bind(new InetSocketAddress(8989)); 
  22.         //启动处理器 
  23.         new Reactor(selector).run(); 
  24.     } 

总结

单线程下的NIO存在性能瓶颈,当某一计算过程缓慢的时候会阻塞住整个线程,导致影响其他事件的处理!

为了解决这一缺陷,我们提出了使用异步线程的方式去操作任务,将耗时较长的业务,封装为一个异步任务,提交到线程池执行!

 

为了使业务操作和新连接接入完全分离开,我们做了另外一重优化,我们封装了一个选择器组,轮询的方式获取选择器,每一个选择器都能够处理多个新连接, socket连接->selector选择器 = 多 -> 1,在每一个选择器里面又可以使用线程池来处理任务,进一步提高吞吐量!

 

责任编辑:武晓燕 来源: 源码学徒
相关推荐

2021-03-05 07:38:52

C++线程编程开发技术

2023-06-05 07:56:10

线程分配处理器

2023-06-06 08:17:52

多线程编程Thread类

2023-06-13 13:39:00

多线程异步编程

2011-08-18 17:07:23

IOS开发多线程NSInvocatio

2009-08-17 16:56:51

C#多线程控制进度条

2013-08-21 16:17:09

iPhone多线程

2011-12-15 11:03:21

JavaNIO

2023-06-08 08:21:08

多线程编程线程间通信

2016-10-09 20:15:30

多线程多进程

2018-04-20 14:11:27

多线程死锁乐观锁

2023-11-03 07:50:01

2011-12-08 13:04:06

JavaNIO

2010-01-28 09:55:05

性能优化

2009-08-17 14:08:33

C#进度条使用

2021-02-26 20:55:56

JavaNIO随机

2021-06-10 00:13:43

C#队列数据

2021-03-26 05:54:00

C#数据方法

2022-02-14 15:07:48

进程FileChanne线程

2015-11-18 18:56:36

Java多线程处理
点赞
收藏

51CTO技术栈公众号