《Netty权威指南》笔记——Netty高级特性
第20章 Netty架构剖析
Reactor通信调度层
该层的主要职责就是监听网络读写和连接操作. 负责将网络层的数据读取到内存缓冲区中, 然后出发各种网络事件, 然后触发各种网络事件, 例如连接创建、连接激活、 读事件、写事件等, 将这些事件触发到PipeLine中, 由PipeLine管理的职责链进行后续处理
由一系列辅助类完成
- 包括reactor线程NioEventLoop及其父类,
- NioSocketChannel/NioServerSocketChannel以及其父类
- ByteBuffer以及由其衍生出来的各种Buffer
- Unsafe以及其衍生出的各种内部类等.
职责链 ChannelPipeline
负责事件在职责链中的有序传播, 同时负责动态的编排. 可以选择监听和处理自己关心的事件, 它可以拦截处理和向后/向前传播事件. 不同应用的Handler节点的功能也不同,
通常情况下, 往往会开发编解码Handler, 可以将外部的协议消息转换成内部的POJO对象, 这样上层业务则只需要关心处理业务逻辑即可, 不需要感知底层的协议差异和线程模型差异, 实现了架构层面的分层隔离
业务逻辑编排层
- 纯粹的业务逻辑编排
- 其他的应用层协议插件, 用于特定协议相关的会话和链路管理.
关键架构质量属性
高性能
Netty高性能的具体实现:
- 采用异步非阻塞的I/O类库, 基于Reactor模式实现, 解决了传统同步阻塞I/O模式下一个服务端无法平滑地处理线性增长的客户端的问题
- TCP接收和发送缓冲区使用直接内存代替堆内存, 避免了内存复制, 提升I/O读取和写入的性能.
- 支持通过内存池的方式循环利用ByteBuf, 避免了频繁创建和小会ByteBuf带来的性能损耗
- 可配置的I/O线程数, TCP参数等, 为不同的用户场景提供定制化的调优参数, 满足不同的性能场景
- 采用环形数组缓冲区实现无锁化并发编程, 代替传统的线程安全容器或者锁
- 合理地使用线程安全容器、原子类等, 提升系统的并发处理能力
- 关键资源的处理使用单线程串行化的方式, 避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题
- 通过引用计数器及时地申请释放不再被引用的对象, 细粒度的内存管理降低了GC的频率, 减少了频繁GC带来的时延增大和CPU损耗
可靠性
-
链路有效性检测
链路由长连接建立, 不需要每次创建, 可以一直保持
相应产生的问题, 比如链路空闲问题,也被心跳机制所解决. 为了支持心跳, Netty提供了两种链路空闲检测机制
- 读空闲超时机制: 当连续周期T没有消息可读时, 触发超时Handler用户可以基于读空闲超时发送心跳消息, 进行链路检测; 如果连续N个周期仍然没有读取到心跳消息, 可以主动关闭链路
- 写空闲超时机制: 当连续周期T没有消息要发送时, 触发超时Handler, 用户可以基于写空闲超时发送心跳信息, 进行链路检测; 如果连续N个周期
-
内存保护机制
Netty提供多种机制对内存进行保护, 包括一下几个方面
- 通过对象引用计数器对Netty的ByteBuf等内置对象进行细粒度的内存申请和释放, 对非法的对象引用进行检测和保护
- 通过内存池来重用ByteBuf节省内存
- 可设置的内存容量上限, 包括ByteBuf、线程池线程数等
-
优雅停机
指的是当系统退出时, JVM通过注册的ShutdownHook拦截到退出的信号量, 然后执行退出操作, 释放相关模块的资源占用, 将缓冲区的消息处理完成或者清空, 将待刷新的数据持久化道磁盘或者数据库中, 等到资源回收和缓冲区消息处理完成之后, 再退出.
可定制性
- 责任链模式: ChannelPipeline 基于责任链模式开发, 便于业务逻辑的拦截、 定制和扩展
- 基于接口的开发: 关键的类库都提供了接口或者抽象类, 如果Netty自身的实现无法满足用户的需求, 可以由用户自定义实现相关接口
- 提供了大量工厂类, 通过重载这些工厂类可以按需创建出用户实现的对象
- 提供了大量的系统参数供用户按需设置, 增强系统的场景定制性
可拓展性
基于Netty的基础NIO框架, 可以方便地进行应用层协议定制, 例如HTTP协议栈、Thrift, FTP协议栈, 这些都不需要修改Netty源码, 直接基于Netty的二进制类库进行协议的拓展和定制
第21章 Java多线程编程在Netty中的应用
Netty的并发编程实践
对共享的可变数据进行正确的同步
synchronized保证同一时刻, 只有一个线程可以执行某一个方法或者代码块. 同步的作用不仅仅是互斥,它的另一个作用就是共享可变性, 当某个线程修改了可变数据并释放锁后, 其他线程可以获取被修改变量的最新值. 如果没有正确的同步, 这种修改对其他线程是不可见的.
由于 ServerBootStrap 是被外部使用者创建和使用的, 我们无法保证它的方法和成员变量不被并发访问. 因此作为成员变量的options必需进行正确的同步.
正确使用锁
-
wait
方法用来让线程等待某种条件,它必须在同步块内部被调用, 这个同步块通常会锁定当前对象实例.synchronized(this) { while(condition) Object.wait; }
-
始终使用
while
循环来调用wait
方法, 永远不要在循环之外调用wait
方法. 原因是尽管并不满足被唤醒条件, 但是由于其他线程调用 notifyAll() 方法会导致被阻塞线程意外唤醒, 此时执行条件并不满足, 它将破坏被锁保护的约定关系, 导致约束失效, 引起意想不到的结果 -
唤醒线程, notify vs notifyAll?
保守起见是调用notifyAll 唤醒所有等待的线程.
volatile的正确使用
- 线程可见性: 当一个线程修改了被volatile修饰的变量后, 无论是加锁, 其他线程都可以立即看到最新的修改
- 禁止指令重排序优化
volatile不能代替传统锁, 答案是不能. volatile仅仅解决了可见性的问题, 但是它并不能保证互斥性, 多个线程并发修某个变量时, 依旧会产生多线程问题.
应用场景来说, volatile最适合使用的是一个线程写, 其他线程读的场合. 如果有多个线程并发操, 仍然需要使用锁或者是线程安全的容器或是源自变量来代替.
CAS指令和原子类
线程安全类的应用
JUC 可以分为4类:
- 线程池 Executor Framework 以及定时任务相关的类库, 包括Timer等
- 并发集合, 包括List、 Queue、Map 和 Set等
- 新的同步器, 如ReadWriteLock
- 新的原子包装类, 如 AtomicInteger
建议通过使用线程池, task(Runnable/callable), 原子类和线程完全容器来代替传统的同步锁, wait 和 notify, 以提升并发访问的性能, 降低多线程编程的难度
NioEventLoop是I/O线程, 负责网络读写操作, 同时也执行一些非I/O的任务. 例如时间通知, 定时任务执行等. 需要一个任务队列来缓存这些Task.
NioEventLoop是 ConcurrentLinkedQueue. JDK的线程安全容器底层采用了CAS, volatile 和 ReadWriteLock实现, 相比起同步锁, 采用了更轻量, 细粒度的锁, 因此,性能会更高. 合理地应用这些线程安全容器.能提升多线程并发访问的性能, 还能降低开发难度.
Netty对线程池的应用
-
定义一个标准的线程池用于执行任务
private final Executor executor;
-
赋值并且进行初始化操作
this.addTaskWakesUp = addTaskWakesUp; this.executor = executor; taskQueue = newTaskQueue();
-
执行任务代码
public void execute(Runnable task){ if(task == null){ throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if(inEventLoop){ // adding taskto eventloop addTask(task); } else{ // start new thread and adding taskto eventloop startThread(); addTask(task); if (isShutdown() && removeTask(task)){ reject(); } } }
-
startThread()
, singleThreadEventExecutor 启动新的线程private void startThread(){ synchronized (stateLock){ if(state == ST_NOT_STARTED)