JVM的多路复用器实现原理

Linux 2.5以前:select/poll

Linux 2.6以后: epoll

Windows: Winsock的select模型(感谢评论指正,仅Java NIO.2使用了Windows IOCP,由于Netty没有采用NIO.2此处不展开)

Free BSD, OS X: kqueue

下面仅讲解Linux的多路复用。

Linux中的IO

Linux的IO将所有外部设备都看作文件来操作,与外部设备的操作都可以看做文件操作,其读写都使用内核提供的系统调用,内核会返回一个文件描述符(fd, file descriptor),例如socket读写使用socketfd。描述符是一个索引,指向内核中一个结构体,应用程序对文件的读写通过描述符完成。

一个基本的IO,涉及两个系统对象:调用这个IO进程的对象,系统内核,read操作发生时流程如下:

通过read系统调用向内核发起读请求。

内核向硬件发送读指令,并等待读就绪。

内核把将要读取的数据复制到描述符所指向的内核缓存区中。

将数据从内核缓存区拷贝到用户进程空间中。

Linux I/O模型简介

阻塞I/O模型:最常用,所有文件操作都是阻塞的。

非阻塞I/O模型:缓冲区无数据则返回,一般采用轮询的方式做状态检查。

I/O复用模型:详细见下

信号驱动I/O:使用信号回调应用,内核通知用户何时开启一个I/O操作。

异步I/O:内核操作完成后进行通知,内核通知用户何时完成一个I/O操作。

Linux IO 多路复用

使用场景

客户处理多个描述符(交互输入,网络套接口)

客户处理多个套接口(少见)

TCP服务器既要处理监听套接口,又要处理已连接套接口。

一个服务器既要处理TCP,又要处理UDP

一个服务器处理多个服务/多个协议

与多进程/多线程对比

I/O多路复用系统开销小,系统不必创建进程/线程,也不需要维护这些进程/线程。

系统调用

目前支持I/O多路复用的系统调用包括select,pselect,poll,epoll,I/O多路复用即通过一种机制,一个进程可以监视多个描述符,一旦某个描述符准备就绪,就能够通知程序进行相应的读写操作。

select/poll

select目前在所有平台支持,select函数监视文件操作符(将fd加入fdset),循环遍历fdset内的fd获取是否有资源的信息,若遍历完所有fdset内的fd后无资源可用,则select让该进程睡眠,直到有资源可用或超时则唤醒select进程,之后select继续循环遍历,找到就绪的fd后返回。select单个进程打开的fd有一定限制,由FD_SETSIZE设置,默认为1024(32位)和2048(64位)。

poll与select的主要区别是不使用fdset,而是使用pollfd结构(本质链表结构),因而没有fd数目限制。

poll和select共有的问题:

每次select/poll找到就绪的fd,都需要把fdset/pollfd进行内存复制。

select/poll,都要在内核中遍历所有传递来的fd来寻找就绪的fd,随着监视的fd数量增加,效率也会下降。

epoll

Linux 2.6内核中提出了epoll,epoll包括epoll_create,epoll_ctl,epoll_wait三个函数分别负责创建epoll,注册监听的事件和等待事件产生。

epoll每次注册新的事件到epoll中时,都会把所有fd拷贝进内核,而不是在epoll_wait时重复拷贝,保证每个fd在整个过程中仅拷贝一次。此外,epoll将内核与用户进程空间mmap到同一块内存,将fd消息存于该内存避免了不必要的拷贝。

epoll使用事件的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就通过回调函数把就绪的fd加入一个就绪链表,唤醒epoll_wait进入睡眠的进程,epoll_wait通知消息给应用程序后再次睡眠。因此epoll不随着fd数目增加效率下降,只有活跃fd才会调用回调函数,效率与连接总数无关。

epoll没有最大并发连接的限制,1G内存约能监听10万个端口。

epoll有LT模式和ET模式:

LT模式:epoll_wait检测到fd并通知后,应用程序可以不立刻处理,下次调用epoll_wait,会再次通知;

ET模式:应用程序必须立刻处理,下次调用,不会再通知此事件。ET模式效率更高,epoll工作在ET模式下必须使用非阻塞套接字。

性能对比

如果有大量的idle-connection或dead-connection,epoll效率比select/poll高很多。

连接少连接十分活跃的情况下,select/poll的性能可能比epoll好。

Java的IO模式

BIO:即传统Socket编程,线程数:客户端访问数为1:1,由于线程数膨胀后系统性能会急剧下降,导致了BIO的低效。

伪异步I/O:为了解决一个链路一个线程的问题,引入线程池处理多个客户端接入请求,可以灵活调配线程资源,可以限制线程数量防止膨胀,但底层仍是阻塞模型,高客户端访问时,会有通信阻塞的问题。

NIO:Java NIO的核心为Channels, Buffers, Selectors。Channel有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel内。而Selector则被用于多路复用,Java NIO可以把Channel注册到Selector上,之后,Selector会获取进入就绪状态的Channel(Selector进行循环的select/poll/epoll/IOCP操作),并进行后续操作。Selector是NIO实现的关键。Java NIO编程较为复杂。

AIO:NIO.2引入的异步通道概念,不需要多路复用器对注册的通道轮询即可异步读写,简化了NIO的编程。(但是Netty作者称AIO的性能并不比NIO和epoll好)

Netty

使用Netty而非直接使用Java NIO出于以下原因:

Java NIO的API过于繁杂。

Java NIO开发需要了解Reactor模型,Java多线程等。

Java NIO低可靠性。

Java NIO有很多臭名昭著的BUG,如NIO的epoll空轮询bug

下面简单介绍下Netty的部分功能。

ByteBuf

Netty的ByteBuf依然是Byte数组缓冲区,提供对基础类型,byte[]数组,ByteBuffer,ByteBuf的读写,缓冲区自身的copy和slice,操作指针,字节序,构造实例等功能。相对于ByteBuffer,ByteBuf的读写采用两个指针而非flip方案,增加了可靠性,并提供了自动扩展方案。

ByteBuf的内存池实现比较复杂,但是否使用内存池,有较大的性能差异。随着JVM和JIT的发展,对象的分配和回收是个轻量级的工作,但是对于缓冲区Buffer,特别是堆外直接内存的分配和回收则仍很耗时。Netty提供了基于内存池的缓冲区重用机制,带来了性能提高。UnpooledByteBufAllocator在Netty4仍然是默认的allocator,但在大多情况下,PooledByteBufAllocator将带来更高性能。更改默认方式仅需在初始化时加以设置:

客户端

b.group(group)

.channel(NioSocketChannel.class)

.option(ChannelOption.TCP_NODELAY, true)

.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)

服务端

.childHandler(new ChannelInitializer() {

@Override

public void initChannel(SocketChannel ch) throws Exception {

ch.config().setAllocator(PooledByteBufAllocator.DEFAULT);

Channel和Unsafe

Netty的Channel和NIO的Channel类似,但有自己的子类和实现。Unsafe则封装了Netty不希望用户调用的API,作为Channel的辅助类。

Channel包括而不限于网络的读,写,客户端发起连接,主动关闭连接,链路关闭,获取双方通信地址等功能。Channel也包括了Netty框架的相关功能,如获取该Channel的EventLoop,获取缓冲区分配器ByteBufAllocator和Pipeline等。Channel封装了Java NIO不统一的SocketChannel和ServerSockerChannel,其接口定义大而全。

Unsafe是Channel的辅助接口,实际的I/O读写操作都是由Unsafe完成的。包括register,bind,disconnect,close,write,flush几个接口,可以看到它更接近于原本的Java NIO Channel。

ChannelPipeline和ChannelHandler

Netty的pipeline和handler机制类似于Servlet和Filter,为了方便拦截和业务逻辑定制。Netty将Channel的管道抽象为ChannelPipeline,让消息在其中流动,ChannelPipeline持有消息拦截器ChannelHandler列表,可以通过增加和删除handler来改变业务逻辑,而不是对已有的handler进行修改。

ChannelHandler的种类繁多,且用户可以自定义,自定义时,通常只需要继承ChannelHandlerAdapter并重写为了实现业务逻辑的必要方法即可。

此外,ChannelPipeline支持运行时动态添加或删除ChannelHandler,某些场景下这个特性很实用。

ChannelPipeline是线程安全的,但ChannelHandler不是线程安全的,需要用户自己进行保障。

EventLoop和EventLoopGroup

Netty的线程模型得以无锁化依赖于其NioEventLoop。因此,此处详细展开。

Netty的线程模型

Reactor模型:所有I/O都在NIO线程完成,NIO线程作为服务端,接收所有客户端TCP连接,并处理链路。

Reactor多线程模型:由专门的一个Acceptor线程监听服务端,接收客户端的TCP请求,并调度一个subReactor线程池,该线程池维护多个处理线程,一个NIO线程可以处理N条链路。

主从Reactor多线程模型:在上述Reactor多线程模型基础上,服务端接收客户端TCP请求的不再是一个NIO线程,而是一个独立的NIO线程池,Acceptor线程池仅用于客户端的登录和认证,链路建立成功就交给subReactor线程池做后续操作。

Netty线程池:服务端启动时,创建bossGroup, workerGroup两个NioEventLoopGroup,实际上是两个Reactor线程池,一个用于接收客户端TCP请求,一个用于处理I/O读写或执行业务。

接收线程池(bossGroup)职责:接收客户端TCP连接,初始化Channel参数,将链路状态变更通知给ChannelPipeline。

I/O处理线程池(workerGroup)职责:异步读取通信对端数据,发送读事件给ChannelPipeline;异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;执行业务或系统调用/定时任务等工作。

通过调整bossGroup和workerGroup的线程个数,group()函数参数数量,是否共享线程池等,Netty的Reactor模型可以在单线程,多线程,主从多线程等模式中切换。

NioEventLoop

Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead方法,只要用户不切换线程,一直都由NioEventLoop调用用户的Handler,期间不切换线程,而是串行化运行handler,避免了多线程操作的锁的竞争,达到性能最优。

NioEventLoop不纯粹是一个IO线程,它既可以处理系统Task又可以处理定时任务。

Future和Promise

Future起源于JDK的Future,Netty的Future命名为ChannelFuture,与Channel操作有关。Netty中所有操作都是异步的,因此,获取异步操作结果,就要交给ChannelFuture。ChannelFuture有completed何uncompleted两种状态,创建后处于uncompleted状态,一旦I/O操作完成,则被设置成completed状态,此时可能操作失败,操作成功或操作被取消。和JDK的Future类似,ChannelFuture有很多方便的API,包括获取操作结果,添加事件监听器,取消I/O操作,同步等待等。

Promise是可写的Future,用于设置I/O额结果。Netty发起I/O操作时,会创建一个新的Promise对象。

参考文献

相关阅读:NIO.2

linux java 多线程_Java多线程:Linux多路复用,Java NIO与Netty简述相关推荐

  1. linux java多线程_Java多线程从简单到复杂

    1 输出数字 (1) 多线程操作全局变量任务 package c; /** * @author ZTX * (1) 多线程操作全局变量任务 * 2013-12-14 21:49:17 * * */ p ...

  2. java几种多线程_Java多线程实现的几种方式

    Java多线程实现的几种方式 多进程是计算机中的一个重要概念,通常一个任务称为一个进程,比如浏览网页.播放音乐都是一个进程. 在进程内部可能还需要执行多个子任务,比如在使用word文档打字的时候,不仅 ...

  3. java厨房_Java多线程基础

    目录: 进程和线程 为什么使用多线程? 多线程的创建方式 Runnable与Thread两种方式比较 start()与run()方法 线程的生命周期/状态转换 常用方法使用与解读 线程的优先级 守护线 ...

  4. linux shell java 参数_java调用linux中的shell脚本传递参数并返回执行结果

    [刚接触 linux下的开发,最近遇到java调用shell脚本的问题,找到一个比较适合菜鸟级的方法,转述如下: 在需要运行的SHELL脚本第一行添加 #!/bin/sh然后在终端运行  chmod ...

  5. java多线程_Java多线程

    java多线程 Multithreading in Java is a very important topic. I have written a lot about Threads in Java ...

  6. java轮番_java多线程轮流打印数字字母案例代码

    本案例演示3个线程轮流打印输出数字字母:代码如下 package thread; public class ThreadTest { public static void main(String[] ...

  7. java程序_Java程序员必备----Java命令大全

    1.java.exe: ====================== 运行java程序,这个相信每一位用Java的人知道了.2.javac.exe: ====================== 编译 ...

  8. java futher多线程_Java多线程系列--“JUC集合”05之 ConcurrentSkipListMap

    概要 本章对Java.util.concurrent包中的ConcurrentSkipListMap类进行详细的介绍.内容包括: ConcurrentSkipListMap介绍 ConcurrentS ...

  9. java mina多线程_Java多线程基础总结九:Mina窥探(1)

    一直以来的多线程的基础总结都是脱离应用的,但是要说多线程的应用就不能不说Mina.Apache Mina作为一个高性能的Java异步并发网 络通讯框架,其内部的多线程的设计和实现可谓是学习多线程的良药 ...

  10. java 禁止使用多线程_Java多线程(四)-线程状态的转换 - Java 技术驿站-Java 技术驿站...

    一.线程状态 线程的状态转换是线程控制的基础.线程状态总的可分为五大状态:分别是生.死.可运行.运行.等待/阻塞.用一个图来描述如下: 1.新状态:线程对象已经创建,还没有在其上调用start()方法 ...

最新文章

  1. HTTP常用状态码说明
  2. 小明分享|分享8ms平台sigmastar SSD201/SSD202 开源框架的github
  3. 将自己写的windows服务加入到windows集群中
  4. 队列服务 php,php Redis 队列服务的简单示例
  5. ConcurrentHashMap核心原理,这次彻底给整明白了
  6. dom 无法找到 body节点问题
  7. 12020.硬件电路
  8. pom.xml中出现错误提示Failure to transfer org.jboss.weld.jar ...  maven2 was cached in the local repository.
  9. SQL Server 2016 SP1中的新功能和增强功能
  10. 共轭函数Fenchel不等式
  11. 字符串模式匹配sunday算法
  12. python nlpir_NLPIR(ICTCLAS 2013)分词工具Python封装
  13. argmin ,argmax函数
  14. JEECMS-V8.1常用标签及简易操作
  15. Mac OS X 清除DNS缓存
  16. sql网上书店项目的实现
  17. 均匀点云边界检测——密度查找(1/2)
  18. 数据挖掘——航空公司客户分类与价值评估案例
  19. python将不同类型文件分开
  20. 抓取微信群聊人数,并保存到excel表格

热门文章

  1. MediaWiki自动登陆和更新页面
  2. ListView中嵌套checkbox
  3. 脏读,幻读,不可重复读的区别
  4. 蓝桥杯 第几个幸运数 set
  5. Android的硬件缩放技术优化执行效率 Screen.SetResolution
  6. fetch git pull 切换_git中fetch和pull的区别
  7. 单元格中指定内容标红_你一直都不知道,键盘上“F4”在office中的强大功能
  8. 此笔记只作为自身笔记,结构比较混乱,不建议参考,如有需要请访问其他文献,servlet的基础知识和使用
  9. 桑基图 python_手把手教你用 Python 绘制酷炫的桑基图!
  10. python3使用staf问题_python3 使用时遇到的问题