阅读本文约“12 分钟”
适读人群:Java 中级
一、Netty 架构
Rector 通信调度层
监听网络的读写和连接操作,将网络层的数据读取到内存缓冲区;
指责链 ChannelPipeline
1、事件的有序传播、同时负责动态地编排指责链,其可以选择监听处理自己关心的,拦截处理传播事件;
2、往往会开发编解码 Handler 用于消息的编解码,他可以将外呼的协议消息转换为内部 POJO,既上层业务仅需关心业务逻辑,不需要感知底层的协议差异和线程模型差异;
业务逻辑编排层 Service ChannelHandler
1、其他应用层的协议插件,用于特定协议相关的会话和链路管理;
2、业务逻辑编排;
高性能
1、采用异步非阻塞 I /O,基于 Reactor 模式实现;
2、TCP 接收和发送缓冲区使用直接内存代替,避免内存复制;
3、支持内存池循环利用 ByteBuf;
4、环形数组缓冲区实现无锁化并发编程;
5、采用单线程串行化的方式;
可靠性
1、链路有效性检测:读、写空闲超时机制
2、内存保护机制:
(1)通过对象计数器对 Netty 的 ByteBuf 等内置对象进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护;
(2)通过内存池来重用 ByteBuf,节省内存;
(3)可设置的内存容量上限,包括 ByteBuf、线程池线程数;
3、优雅停机:
(1)当系统退出时,JVM 通过注册的 Shutdown Hook 拦截到推出的信号量,然后执行推出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘或者数据库中,等到资源回收和缓冲区消息处理完成之后,再退出;
(2)优雅停机需要设置最大超时时间 T,如果达到 T 后系统仍没有退出,则通过 Kill -9 pid 强杀当前的进程;
可定制性
1、责任链模式:ChannelPipeline 基于责任链模式开发,便于业务逻辑的拦截,定制和扩展;
2、基于接口开发;
3、提供大量的工厂类;
二、Netty 并发编程实现
对共享的可变数据进行正确的同步
关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法或者代码块,同步的作用不仅仅是互斥,另一作用是共享可变性,当某个线程修改了可变数据并释放锁后,其他线程可以获取被修改变量的最新值;
volatile 的正确使用
1、线程可见性:当一个线程修改了被 volatile 修饰的变量后,无论是否加锁,其他线程都可以立即看到最新的修改,而普通变量却做不到;
2、禁止指令重排序优化,普通变量仅仅保证在执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码的执行顺序一致;
3、volatile 不可能代替传统锁,它仅解决了可见性,不能保证互斥,多个线程改变变量,还是会由多线程问题;
4、最适合使用在一个线程写、其他线程读的场合;
CAS 指令和原子类
1、互斥同步主要问题就是进行线程阻塞和唤醒带来的性能的额外损耗,也被称为阻塞同步,一种悲观的并发策略;
2、乐观锁:先进行操作,操作完成之后再判断操作是否成功,是否有并发问题,如果有则进行失效补偿,如果没有就算操作成功;
3、使用 Java 自带的 Atomic 原子类,可以避免同步锁带来的并发访问性能降低的问题,减少犯错的机会;
4、Netty 中对于 int、long、boolean 等成员变量大量使用其原子类,减少了锁的应用,从而降低了频繁使用同步锁带来的性能下降;
线程安全类的应用
1、建议通过线程池、Task(Runnable/Callable)、原子类和线程安全容器来代替传统的同步锁、wait 和 notify、以提升并发访问的性能、降低多线程编程的难度;
2、Netty 对 JDK 的线程池进行了封装和改造,但是,本质上仍然是利用了线程池和线程安全队列简化了多线程编程;
读写锁的应用
1、主要用于读多写少的场景,用来替代传统的同步锁,以提升并发访问性能;
2、读写锁是可重入、可降级,一个线程获取读写锁后,可以继续递归获取,从写锁可以降级为读锁,以便快速释放资源;
3、读写锁支持非阻塞的尝试获取锁,如果获取失败,返回 false,而不是同步阻塞;
4、获取锁之后一定要释放锁,否则会发生锁溢出异常。通常的做法是通过 finally 块释放锁,如果是 trylock,获取锁成功才需要释放锁;
不要依赖线程优先级
在任何情况下,程序都不能依赖 JDK 自带的线程优化级来保证执行顺序、比例和策略;
三、重点知识
无锁化的串行设计
1、为了尽可能地避免锁竞争带来的性能损耗,可以通过串行化设计,既消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁;
2、通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行化行,这种局部无锁化的串行线程设计相比一个队列——多个工作线程模型性能更优;
3、Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换;
4、串行化处理方式避免了多线程操作导致的锁竞争,从性能角度是最优;
零拷贝
1、Netty 的接收和发送 ByteBuffer 采用直接内存,使用堆外直接内存进行 socket 读写,不需要进行字节缓冲区的二次拷贝;
2、CompositeByteBuf,它将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口;
3、文件传输,文件传输类 DafaultFileRegion 通过 transferTo 方法将文件发送到 Channel;
规避 NIO BUG
1、死循环是可检测、可预防但是无法完全避免的;
2、epoll bug,会导致 Selector 空轮询,IO 线程 CPU100%,严重影响系统的安全性和可靠性;
3、Netty 的解决策略
(1)根据该 BUG 的特征,首先侦测该 BUG 是否发生
(2)将问题 Selector 上注册的 Channel 转移到新建的 Selector 上
(3)老问题的 Selector 关闭,使用新建的 Selector 替换