文章目录
多核时代,多线程在高并发上的重要性不必多说
多线程带来的隐患和技术挑战
线程安全
在线程安全类中封装必要的同步机制,保证线程安全性
着重点在访问共享的可变状态时需要进行正确的管理:
- 只读共享,任何线程都可以访问但不能修改对象
- 保护对象,通过持有锁进行访问
- 线程安全共享,对象的内部实现同步
- 线程封闭,使得对象只能由一个线程拥有
活跃性
当某个操作不能继续执行下去的时候,就会发生活跃性问题,最主要的原因是锁顺序死锁的产生。在设计时一定要排除可能导致死锁出现的条件:
- 支持定时锁
- 按照固定的顺序持有锁
其他活跃性危险: - 饥饿
- 糟糕的响应性
- 活锁
性能问题
多线程引入的额外操作的开销包括:线程之间的协调、增加上下文切换、线程的创建和销毁、线程调度等
对于多线程的性能问题,Java提供了一些原生的解决方案:不同的锁策略(synchronized关键字、读写显式锁等)、CAS无锁设计以及线程池。
Disruptor
它是一种可替代有界队列完成并发线程间数据交换的高性能解决方案,Disruptor的设计初衷就是为了解决多线程引入的性能问题【锁代价过大、CAS代价过大、缓存行导致的伪共享、队列存在的问题】,以求最优化内存分配,使用缓存友好的方式来最佳使用现代硬件资源。
内存分配
Ring buffer的内存是在启动时预先分配的。预先分配的策略就能够避免Java内存回收引起的一些性能问题,因为这些元素对象(enries)在能够在Disruptor实例中的整个生命周期存活和被复用【注:这些enties一直被RingBuffer对象持有,而RingBuffer实例对象又被Disruptor持有,故只要Disruptor存在,则这些enties便不会被GC掉】。由于这些对象是在开始阶段同时分配的,很大程度上被连续分布在主存中,即使不连续它们在内存中得间隔也有可能是固定的,从而支持缓存步幅,非常有利于缓存的数据预取【硬件操作缓存并不是以字节或字为单位,为了效率考虑,缓存通常以缓存行(cache line)的形式进行组织,缓存行通常有32-256字节,最常见的是64字节。缓存行也是缓存一致性协议操作的最小粒度】。
隔离关注(Teasing Apart the Concerns)
如下几个分开的关注点是所有队列实现方式都需要综合实现的,这些也是为队列实现定义的接口:
1) 队列元素的存储;
2) 队列协调生产者声明下一个需要交换的队列元素的序号;
3) 队列协调并告知消费者等待的元素已经就绪。
序列化(Sequencing)
顺序化是disruptor管理并发的核心概念。每个生产者和消费者都维护自己的序号,这个序号用来和RingBuffer交互。当一个生产者希望在RingBuffer中添加一个元素时,它首先要做的是声明一个序号【注:这个序号被称之为生产者的声明序号,该序号用来指示下一个空闲的slot的位置,一旦序号被声明,那么该序号就不会被其它生产者重复操作,生产者就可以操作该声明序号的slot数据】。单生产者的场景下,这个声明序号可以是一个简单的整数;多生产者的场景,这个序号必须是一个支持CAS的原子变量。当一个生产者序号被声明,这个序号对应的位置就可以被声明该序号的生产者写入了;当生产者完成更新元素后,它就通过更新一个单独的序号来提交变化,这个单独的序号是一个游标(cursor),用来指示消费者可以消费的最新的元素。
批量效应(Batching Effect)
当消费者等待RingBuffer中可用的前进游标序号时,如果消费者发现RingBuffer游标自上次检查以来已经前进了多个序号,消费者可以直接处理所有可用的多个序号,而不用引入并发机制, 这样滞后的消费者能够迅速跟上生产者的步伐,从而平衡系统
Netty
线程模型
Reactor反应器模式,其实,有很多高性能的中间件都是基于反应器模式进行设计的,包括Nginx、Redis、Netty等。
反应器模式由Reactor反应器线程、Handlers处理器两大角色组成:
- Reactor反应器线程的职责:负责响应IO事件,并且分发到Handlers处理器
- Handlers处理器的职责:非阻塞的执行业务处理逻辑
ChannelPipeline,将绑定到一个通道的多个Handler处理器实例,串在一起,形成一条流水线,实现了对多个处理器的编排(所有通道的事件是串行处理的)。
“零拷贝”的内存优化
Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Netty中大量使用基于“零拷贝”的api实现功能,对内存上的数据进行直接操作。
高效的内存管理
Netty使用ByteBuf对象作为数据容器,进行I/O读写操作,Netty的内存管理也是围绕着ByteBuf对象高效地分配和释放。内存管理上,实现了对内存的池化管理,通过归一化处理,使池化内存分配大小规格化,大大方便内存申请和内存、内存复用,提高效率,然后,通过PoolArena对象,实现对多个PoolChunkList、PoolSubpage的分组管理调度,线程安全控制、提供内存分配、释放的服务。在线程处理上,首先,为了减少线程间的竞争,Netty会提前创建多个PoolArena,当线程首次请求池化内存分配,会找被最少线程持有的PoolArena,并保存线程局部变量PoolThreadCache中,实现线程与PoolArena的关联绑定;Netty还设计了缓存机制提升并发性能:当请求对象内存释放,PoolArena并没有马上释放,而是先尝试将该内存关联的PoolChunk和chunk中的偏移位置(handler变量)等信息存入PoolThreadLocalCache中的固定大小缓存队列中(如果缓存队列满了则马上释放内存),当请求内存分配,PoolArena会优先访问PoolThreadLocalCache的缓存队列中是否有缓存内存可用,如果有,则直接分配,提高分配效率。
本文探讨了多线程编程中的隐患和挑战,如线程安全、活跃性问题和性能开销,并介绍了Disruptor如何通过内存分配和序列化解决这些问题。Disruptor通过预分配内存、序列化管理和批量效应提高性能。此外,文章还提及Netty的线程模型和零拷贝内存优化,展示了其在高性能并发处理中的应用。
1657

被折叠的 条评论
为什么被折叠?



