窥探 Netty 源码!Recycler 对象池实现原理剖析

简介: 该文所涉及的 netty 源码版本为 4.1.6。

本文选自 Doocs 开源社区旗下“源码猎人”项目,作者 tydhot。


项目将会持续更新,欢迎 Star 关注。


项目地址:https://github.com/doocs/source-code-hunter


该文所涉及的 netty 源码版本为 4.1.6。


Netty 的对象池 Recycler 是什么


Recycler 是 Netty 中基于 ThreadLocal 的轻量化的对象池实现。既然是基于 ThreadLocal,那么就可以将其理解为当前线程在通过对象池 Recycler 得到一个对象之后,在回收对象的时候,不需要将其销毁,而是放回到该线程的对象池中即可,在该线程下一次用到该对象的时候,不需要重新申请空间创建,而是直接重新从对象池中获取。


Recycler 在 netty 中被如何使用


Recycler 对象池在 netty 中最重要的使用,就在于 netty 的池化 ByteBuf 的场景下。首先,何为池化?以 PooledDirectByteBuf 举例,每一个 PooledDirectByteBuf 在应用线程中使用完毕之后,并不会被释放,而是等待被重新利用,类比线程池每个线程在执行完毕之后不会被立即释放,而是等待下一次执行的时候被重新利用。所谓的对象池也是如此,池化减少了 ByteBuf 创建和销毁的开销,也是 netty 高性能表现的基石之一。


private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {    @Override    protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {        return new PooledDirectByteBuf(handle, 0);    }};static PooledDirectByteBuf newInstance(int maxCapacity) {    PooledDirectByteBuf buf = RECYCLER.get();    buf.reuse(maxCapacity);    return buf;}


PooledDirectByteBuf 在其类加载的过程中,初始化了一个静态的 RECYCLER 成员,通过重写其 newObject()方法达到使 Recycler 可以初始化一个 PooledDirectByteBuf。而在接下来的使用中,只需要通过静态方法 newInstance()就可以从 RECYCLER 对象池的 get()方法获取一个新的 PooledDirectByteBuf 对象返回,而重写的方法 newObject()中的入参 Handler 则提供了 recycle()方法给出了对象重新放入池中回收的能力,这里的具体实现在下文展开。因此,newInstance()方法和 recycle()方法就提供了对象池出池和入池的能力,也通过此,PooledDirectByteBuf 达到了池化的目标。


Recycler 的实现原理分析


Recycler 的实现原理很抽象,可以先直接阅读文末的例子再阅读这部分内容。


Recycler 中,最核心的是两个通过 ThreadLocal 作为本地线程私有的两个成员,而其实现原理只需要围绕这两个成员分析,就可以对对象池的设计有直接的理解和认识。


第一个成员是在 Recycler 被定义的 Stack 成员对象。


private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {    @Override    protected Stack<T> initialValue() {        return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,                ratioMask, maxDelayedQueuesPerThread);    }};


顾名思义,这个 Stack 主体是一个堆栈,但是其还维护着一个链表,而链表中的每一个节点都是一个队列。


private DefaultHandle<?>[] elements;private WeakOrderQueue cursor, prev;


上述的 elements 数组便是存放当前线程被回收的对象,当当前线程从该线程的 Recycler 对象池尝试获取新的对象的时候,首先就会从当前 Stack 的这个数组中尝试获取已经在先前被创建并且在当前线程被回收的对象,因为当对象池的对象在当前线程被调用 recycle()的时候,是会直接放到 elements 数组中等待下一次的利用。那么问题来了,如果从该线程中被申请的这个对象是在另外一个线程中被调用 recycle()方法回收呢?那么该对象就会处于链表中的队列中,当堆栈数组中的对象不存在的时候,将会尝试把链表队列中的对象转移到数组中供当前线程获取。那么其他线程是如何把被回收的对象放到这些链表中的队列的呢?接下来就是另一个成员的使命了。


第二个成员是在 Recycler 中也是通过 ThreadLocal 所实现的一个线程本地变量,DELAYED_RECYCLED ,是一个 Stack 和队列的映射 Map。


private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =        new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {    @Override    protected Map<Stack<?>, WeakOrderQueue> initialValue() {        return new WeakHashMap<Stack<?>, WeakOrderQueue>();    }};


第二个成员 DELAYED_RECYCLED 可以通过上文的 Stack 获取一个队列。


在前一个成员的解释中提到,当别的线程调用另一个线程的对象池的 recycle()方法进行回收的时候,并不会直接落到持有对象池的线程的 Stack 数组当中,当然原因也很简单,在并发情况下这样的操作显然是线程不安全的,而加锁也会带来性能的开销。因此,netty 在 Recycler 对象池中通过更巧妙的方式解决这一问题。


在前面提到,除了数组,Stack 还持有了一系列队列的组成的链表,这些链表中的每一个节点都是一个队列,这些队列又存放着别的线程所回收到当前线程对象池的对象。那么,这些队列就是各个线程针对持有对象池的专属回收队列,说起来很拗口,看下面的代码。


private void pushLater(DefaultHandle<?> item, Thread thread) {    // we don't want to have a ref to the queue as the value in our weak map    // so we null it out; to ensure there are no races with restoring it later    // we impose a memory ordering here (no-op on x86)    Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();    WeakOrderQueue queue = delayedRecycled.get(this);    if (queue == null) {        if (delayedRecycled.size() >= maxDelayedQueues) {            // Add a dummy queue so we know we should drop the object            delayedRecycled.put(this, WeakOrderQueue.DUMMY);            return;        }        // Check if we already reached the maximum number of delayed queues and if we can allocate at all.        if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {            // drop object            return;        }        delayedRecycled.put(this, queue);    } else if (queue == WeakOrderQueue.DUMMY) {        // drop object        return;    }    queue.add(item);}private WeakOrderQueue(Stack<?> stack, Thread thread) {    head = tail = new Link();    owner = new WeakReference<Thread>(thread);    synchronized (stack) {        next = stack.head;        stack.head = this;    }    // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in    // the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the    // Stack itself GCed.    availableSharedCapacity = stack.availableSharedCapacity;}


pushLater()方法发生在当一个对象被回收的时候,当当前线程不是这个对象所申请的时候的线程时,将会通过该对象的 Stack 直接去通过 DELAYED_RECYCLED 映射到一条队列上,如果没有则创建并建立映射,再把该对象放入到该队列中,以上操作结束后该次回收即宣告结束


private WeakOrderQueue(Stack<?> stack, Thread thread) {    head = tail = new Link();    owner = new WeakReference<Thread>(thread);    synchronized (stack) {        next = stack.head;        stack.head = this;    }    // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in    // the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the    // Stack itself GCed.    availableSharedCapacity = stack.availableSharedCapacity;}


如果在操作中,队列是被创建的,会把该队列放置在 Stack 中的链表里的头结点,保证创建该对象的线程在数组空了之后能够通过链表访问到该队列并将该队列中的回收对象重新放到数组中等待被下次重新利用,队列交给 A 线程的链表是唯一的阻塞操作。在这里通过一次阻塞操作,避免后续都不存在资源的竞争问题。


举一个例子来解释对象池的原理


A 线程申请,A 线程回收的场景。


显然,当对象的申请与回收是在一个线程中时,直接把对象放入到 A 线程的对象池中即可,不存在资源的竞争,简单轻松。


A 线程申请,B 线程回收的场景。


首先,当 A 线程通过其对象池申请了一个对象后,在 B 线程调用 recycle()方法回收该对象。显然,该对象是应该回收到 A 线程私有的对象池当中的,不然,该对象池也失去了其意义。

那么 B 线程中,并不会直接将该对象放入到 A 线程的对象池中,如果这样操作在多线程场景下存在资源的竞争,只有增加性能的开销,才能保证并发情况下的线程安全,显然不是 netty 想要看到的。

那么 B 线程会专门申请一个针对 A 线程回收的专属队列,在首次创建的时候会将该队列放入到 A 线程对象池的链表首节点(这里是唯一存在的资源竞争场景,需要加锁),并将被回收的对象放入到该专属队列中,宣告回收结束。

在 A 线程的对象池数组耗尽之后,将会尝试把各个别的线程针对 A 线程的专属队列里的对象重新放入到对象池数组中,以便下次继续使用。


全文完!


希望本文对大家有所帮助。如果感觉本文有帮助,有劳转发或点一下“在看”!让更多人收获知识!


推荐阅读


窥探 Netty 源码!FastThreadLocal 究竟快在哪里?

牛逼!一文看懂 Java 并发编程在各主流框架中的应用

把 AQS 源码扒个「体无完肤」!

目录
相关文章
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
13498 1
|
5月前
|
存储 设计模式 缓存
面试官:说说Netty对象池的实现原理?
Netty 作为一个高性能的网络通讯框架,它内置了很多恰夺天工的设计,目的都是为了将网络通讯的性能做到极致,其中「对象池技术」也是实现这一目标的重要技术。 ## 1.什么是对象池技术? 对象池技术是一种重用对象以减少对象创建和销毁带来的开销的方法。在对象池中,只有第一次访问时会创建对象,并将其维护在内存中,当再次需要使用对象时,会直接从对象池中获取对象,并在使用完毕后归还给对象池,而不是频繁地创建和销毁对象。 使用对象池技术的优点有以下几个: 1. **提高性能**:复用对象可以减少对象的创建和销毁次数,降低系统开销,提高系统性能和吞吐量。 2. **减少内存碎片**:对象池可以避免
43 0
|
6月前
|
消息中间件 Oracle Dubbo
Netty 源码共读(一)如何阅读JDK下sun包的源码
Netty 源码共读(一)如何阅读JDK下sun包的源码
127 1
|
11月前
|
NoSQL Java Redis
跟着源码学IM(十二):基于Netty打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。 原本打算做个多人斗地主练习程序,但那需要织入过多的业务逻辑,因此一方面会带来不必要的理解难度,让案例更为复杂化,另一方面代码量也会偏多,所以最终依旧选择实现基本的IM聊天程序,既简单,又能加深对Netty的理解。
162 1
|
6月前
|
编解码 前端开发 网络协议
Netty Review - ObjectEncoder对象和ObjectDecoder对象解码器的使用与源码解读
Netty Review - ObjectEncoder对象和ObjectDecoder对象解码器的使用与源码解读
155 0
|
6月前
|
编解码 安全 前端开发
Netty Review - StringEncoder字符串编码器和StringDecoder 解码器的使用与源码解读
Netty Review - StringEncoder字符串编码器和StringDecoder 解码器的使用与源码解读
250 0
|
分布式计算 网络协议 前端开发
【Netty底层数据交互源码】
【Netty底层数据交互源码】
|
Java 容器
【深入研究NIO与Netty线程模型的源码】
【深入研究NIO与Netty线程模型的源码】
|
编解码 弹性计算 缓存
Netty源码和Reactor模型
Netty源码和Reactor模型
101 0
|
设计模式 监控 前端开发
第 10 章 Netty 核心源码剖析
第 10 章 Netty 核心源码剖析
130 0