Netty中真的没有使用锁吗?
Netty号称是一个事件驱动&异步串行无锁化的网络通信框架.
在Netty的官方网站(https://netty.io/)中声称, 它是一个异步的, 事件驱动的网络框架.
关于事件驱动, 在之前的文章中也简单提到过, Netty内部会一直轮询ACCEPT,READ,WRITE,CONNECT等事件, 根据轮询到的不同的事件, 调用不同的方法, 做出不同的响应. 正如我们平时说的, 操作系统是基于中断驱动的, 而Netty是基于事件驱动的.
关于异步这块, 我给它的准确定义是异步串行无锁化. 然而在Netty内部会包含两类线程, 一类是执行IO操作的IO线程, 比如执行上面说的各种事件的线程就是IO线程. 还有一类线程是非IO线程(这不是废话嘛), 也就是业务线程. 虽然我说它是异步串行无锁化, 但不准确, 因为我没说主语. 到底是IO线程在异步串行无锁化, 还是非IO线程在异步串行无锁化呢? 这里说的是非IO线程, 非IO线程在执行写操作的时候, 会把写操作封装成一个写任务, 然后提交到与IO线程唯一绑定的任务队列里, 由IO线程从队列里面取出任务去执行. 而非IO线程提交完任务之后就返回了, 可以继续向下执行. 而且即便多个非IO线程同时向任务队列中提交任务, 也不会发生阻塞, 也不会加锁, 因为它是通过CAS方式操作队列的. 这个高性能的队列, 自然和JDK自带的无缘, 它是jctools里的一个队列(org.jctools.queues.MpscUnboundedArrayQueue), 专为并发而生的队列. 任务被提交到任务队列之后, IO线程就会从队列中取出任务, 逐个串行执行.
IO线程一直无限循环地沉浸在轮询IO事件-处理IO事件-执行队列中的任务这三件事情无法自拔.在这个过程中, IO线程也没有使用加锁的逻辑.
那么在Netty中到底哪里会使用加锁的逻辑呢? 是在申请内存的时候.
举例来说, 当网卡接收到数据之后, 通过中断通知CPU, CPU响应硬中断, 同时发起软中断请求. 操作系统的ksoftirqd线程执行软中断, 将网络数据通过协议栈处理, 最终放到socket的接收缓冲区, 唤醒IO线程(IO线程可能执行了epoll处于阻塞状态). IO线程就会读取数据, 然而数据肯定需要内存空间来保存. 这个时候IO线程就会申请堆外空间进行存储这些数据. 在申请堆外空间的时候可能就会发生加锁的情况. 关于内存申请和释放这块, 在接下来的文章中就会介绍到. 这里简单描述下IO线程申请内存空间的流程.
如上图所示, 当IO线程申请内存的时候, 首先会从自身的PoolThreadCache中查找是否有可用的空闲内存, 这个时候是不需要加锁的, 因为每个IO线程都有一个属于自己的PoolThreadCache. 当PoolThreadCache无可用的内存时, 这个时候就会从PoolSubpage中查找空闲内存, 这个时候就要加锁了, 因为Arena是线程共享的, PoolSubpage也是线程共享的, 这个时候加锁使用synchronized(poolSubpage) {...} , 目前的加锁力度并不是很大, 只有两个IO线程申请相同大小的内存空间, 就会向相同的PoolSubpage申请空间, 这个时候这两个IO线程才会使用同一把锁, 如果两个IO线程都需要向PoolSubpage申请空间, 但是是在不同的PoolSubpage中申请空间, 那么它们使用的是不同的锁. 假如PoolSubpage也没有适合的空闲空间, 那么就需要向Chunk申请了, 这个时候, 如果两个IO线程共享的是同一个Arean, 那么如果它们都需要向Chunk申请空间, 那么它们使用相同的锁, 即synchronized(this) {...}, 这里的this就是Arena.
此篇文章只是简单说了下, 在Netty中在申请内存空间的时候可能会存在加锁的情况, 以及申请内存的大概流程, 更详细的内存申请会在接下来的文章中说到, 而且内存申请是比较难理解的一块内容, 希望通过我的解释, 到时候能让你有所收获. 为今天的你, 加个油!