一、作用&思想
1.1 作用
- 当出现频繁创建对象,销毁对象的时候,为了提高性能,可以将其缓存,减少对象创建的频次;
1.2 思想
- 并发、串行、无锁化思想贯穿其中
- 空间换时间思想,提高运行效率
- 规则约束,避免空间被无限增大,出现意想不到的情况;比如:长度约束;最终可见性(而不是实时可见性)约束
- 谁创建谁回收重利用,规则清晰
二、源码中使用场景
- PooledByteBuf对象
- ChannelOutboundBuffer中的Entry对象
后面以PooledByteBuf为例,进行讲解Recycler的运行原理
三、简单用例
当前是低版本的用例,4.1.38版本;高版本的话, 把Recycler替换成ObjectPool(语义上更加清晰)
java
static class Student {
private static final Recycler<Student> recycler = new Recycler<Student>() {
@Override
protected Student newObject(Handle<Student> handle) {
return new Student(handle);
}
};
private final Handle<Student> handle;
public String name = "zhangsan";
public int age = 1;
public Student(Handle<Student> handle) {
this.handle = handle;
}
public void recyle() {
handle.recycle(this);
}
public void setName(String name) {
this.name = name;
}
public static Student getInstance() {
return recycler.get();
}
}
四、4.1.38版本低版本
注:规定一些术语定义
- 创建线程:创建PooledByteBuf对象的线程,PooledBytebuf.getInstance()
- 回收线程:回收该PooledByteBuf对象的线程,bytebuf.release()
- 该定义源引自大佬文章[1]
4.1 整体类图结构
4.2 整体流程
4.2.1 释放对象流程
4.2.1.1 释放对象的流程图
4.2.1.2 源码解析
io.netty.util.Recycler.Stack#push
- 比较创建线程和当前线程是否一致;
- 如果一致,说明同一线程,则直接调用pushNow,放到stack中;(比较简单,就不展开了)
- 如果不一致,则调用pushLater;(不同线程如何回收才是重点)
java
void push(DefaultHandle<?> item) {
Thread currentThread = Thread.currentThread();
if (threadRef.get() == currentThread) {
// The current Thread is the thread that belongs to the Stack, we can try to push the object now.
pushNow(item);
} else {
// The current Thread is not the one that belongs to the Stack
// (or the Thread that belonged to the Stack was collected already), we need to signal that the push
// happens later.
pushLater(item, currentThread);
}
}
io.netty.util.Recycler.Stack#pushLater
- DELAYED_RECYCLED是一个FastThreadLocal,也就是说每个线程都有一个Map映射表,用于记录Stack和WeakOrderQueue之间的关系;
- 从delayedRecycled获取当前stack对应的WeakOrderQueue,准备将item放入这个WeakOrderQueue
- 如果queue==null,且如果说明还没有初始化,如果还大于maxDelayQueues,说明超限了,不能放了;否则就开始创建;创建是一个重点,(敲黑板)
- 然后将这个创建的WeakOrderQueue加入到delayedRecycled中
- 最后将item加入到queue中
java
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>();
}
};
private void pushLater(DefaultHandle<?> item, Thread thread) {
Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
WeakOrderQueue queue = delayedRecycled.get(this);
if (queue == null) {
if (delayedRecycled.size() >= maxDelayedQueues) {
delayedRecycled.put(this, WeakOrderQueue.DUMMY);
return;
}
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);
}
io.netty.util.Recycler.WeakOrderQueue#allocate
- 如果
availableSharedCapacity-LINK_CAPACITY>0
,说明空间还够分配,否则就不分配; - 分配时,先创建一个WeakOrderQueue,同时设置当前stack的head指针指向这个queue;那么最终形成的结构图如下图所示
arduino
static WeakOrderQueue allocate(Stack<?> stack, Thread thread) {
return Head.reserveSpace(stack.availableSharedCapacity, LINK_CAPACITY)
? newQueue(stack, thread) : null;
}
static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
final WeakOrderQueue queue = new WeakOrderQueue(stack, thread);
// Done outside of the constructor to ensure WeakOrderQueue.this does not escape the constructor and so
// may be accessed while its still constructed.
stack.setHead(queue);
return queue;
}
最终形成的结构图:
每个线程持有该stack里面的一个WeakOrderQueue,通过这个Queue,将其他线程回收的对象放到并行无阻塞的放入到WeakOrderQueue中;
io.netty.util.Recycler.WeakOrderQueue#add
- 如果writeIndex==LINK_CAPACITY的话,说明当前的link已经满了;需要申请新空间;此时如果空间还不足以申请LINK_CAPACITY大小,直接丢弃即可;否则新创建一个Link,将tail指针后移,指向这个Link对象;
- 将这个元素加入到tail的elements中
- 长度加1
java
void add(DefaultHandle<?> handle) {
handle.lastRecycledId = id;
Link tail = this.tail;
int writeIndex;
if ((writeIndex = tail.get()) == LINK_CAPACITY) {
if (!head.reserveSpace(LINK_CAPACITY)) {
// Drop it.
return;
}
this.tail = tail = tail.next = new Link();
writeIndex = tail.get();
}
tail.elements[writeIndex] = handle;
handle.stack = null;
tail.lazySet(writeIndex + 1);
}
所以对象被release后,形成的最终结构图如下所示:
4.2.2 对象申请流程
4.2.2.1 对象申请的流程图
1、同线程中获取回收线程流程
2、从其他线程中获取回收对象流程
4.2.2.2 源码解析
io.netty.util.Recycler#get
- 从threadlocal获取当前的stack对象;
- 从stack中弹出一个元素,如果为空,则说明没有可重用的对象,新建一个Handle对象;
- 返回handle中包装的value对象
ini
public final T get() {
if (maxCapacityPerThread == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle);
}
return (T) handle.value;
}
io.netty.util.Recycler.Stack#pop
- 如果当前stack中size=0,说明可重用对象数量=0
- 执行scavenge方法,从其他线程扫描
- 然后从elements中获取可重用元素,
- 同时将
elements[size]=null
, 这一块很精巧,这样就能保证从elements里面获取一定是有值(无论是null还是非null),因为从pop返回以后,在get方法里面会有一个==null
的判断,根据非null条件来判断是否需要创建对象;
ini
DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
if (!scavenge()) {
return null;
}
size = this.size;
}
size --;
DefaultHandle ret = elements[size];
elements[size] = null;
if (ret.lastRecycledId != ret.recycleId) {
throw new IllegalStateException("recycled multiple times");
}
ret.recycleId = 0;
ret.lastRecycledId = 0;
this.size = size;
return ret;
}
io.netty.util.Recycler.Stack#scavenge
- 如果能扫描到,直接返回true;
- 如果没有扫描到,将prev置为null, cursor置为head;(cursor代表当前节点,prev代表当前节点的前一个节点,head代表头结点, 遍历是否有重用对象是从cursor逐步向后移动)
typescript
boolean scavenge() {
// continue an existing scavenge, if any
if (scavengeSome()) {
return true;
}
// reset our scavenge cursor
prev = null;
cursor = head;
return false;
}
io.netty.util.Recycler.Stack#scavengeSome
重头戏,逐行解析
- 从4.2.1.2章节看到的最终结构图中了解到,当回收完成后,一个Stack持有WeakOrderQueue的链表,每一个WeakOrderQueue代表一个其他线程帮创建线程回收的对象;
- head 代表头指针
- cursor 代表当前待操作的指针; 疑问:为什么cursor和head不能合二为一呢?
- prev 代表当前指针的前一个位置
- owner是一个WeakReference,一旦为null,则说明被GC了,那这个线程里面的所有被回收的线程都需要被移走;
java
boolean scavengeSome() {
WeakOrderQueue prev;
// 获取当前指针
WeakOrderQueue cursor = this.cursor;
// 指针为空,说明可能是第一次执行
if (cursor == null) {
prev = null;
cursor = head;
// 如果赋值head后,也为空,说明当前没有元素,不用在扫描了;
if (cursor == null) {
return false;
}
} else {
prev = this.prev;
}
boolean success = false;
do {
// 从cursor执行的WeakOrderQueue中进行转移元素,重点!!!
if (cursor.transfer(this)) {
success = true;
break;
}
WeakOrderQueue next = cursor.next;
// 说明被GC掉了,那这个线程被干掉了,里面的数据也需要全量被迁移走;
if (cursor.owner.get() == null) {
if (cursor.hasFinalData()) {
for (;;) {
if (cursor.transfer(this)) {
success = true;
} else {
break;
}
}
}
if (prev != null) {
prev.setNext(next);
}
} else {
prev = cursor;
}
// 如果在当前cursor中没有找到, prev、cursor后移一位,
cursor = next;
// 直至将链表中元素遍历完成,或者找到执行元素
} while (cursor != null && !success);
// prev、cursor进行赋值回Stack对象;
this.prev = prev;
this.cursor = cursor;
return success;
}
io.netty.util.Recycler.WeakOrderQueue#transfer
这个方法也是贼长,核心点
- 如果是最后一个LINK,且readIndex = LINK_CAPACITY,则返回false,同时回收空间
- 否则获取一个LINK中所有元素,将其迁移到Stack的elements中;
- 如果stack的elements长度不够,则需要扩容;
ini
boolean transfer(Stack<?> dst) {
// 从数据节点(LINK)开始扫描,
Link head = this.head.link;
if (head == null) {
return false;
}
// 如果当前节点已经读完了
if (head.readIndex == LINK_CAPACITY) {
// 且没有下一个LINK了,则返回false;
if (head.next == null) {
return false;
}
// 指针后移一位
this.head.link = head = head.next;
// 回收这一块由回收线程占用的空间
this.head.reclaimSpace(LINK_CAPACITY);
}
// 需要读的起点(有回收对象的起点)
final int srcStart = head.readIndex;
// 有回收对象的终点(writeIndex)
int srcEnd = head.get();
// 需要迁移的数据的个数;
final int srcSize = srcEnd - srcStart;
// 如果没有数据,异常情况,返回false
if (srcSize == 0) {
return false;
}
// 当前Stack中元素的个数
final int dstSize = dst.size;
// 一旦迁移到Stack,那么Stack最少要达到expectedCapacity
final int expectedCapacity = dstSize + srcSize;
// 如果expectedCapacity 大于 Stack的elements的阈值,则需要扩容;
if (expectedCapacity > dst.elements.length) {
// 扩容大小是之前的2倍;
final int actualCapacity = dst.increaseCapacity(expectedCapacity);
// 最多查看迁移的个数;因为有可能Stack的elements可能没有那么多空间了;
srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);
}
// 如果起点和终点不一样,说明有需要回收的元素
if (srcStart != srcEnd) {
final DefaultHandle[] srcElems = head.elements;
final DefaultHandle[] dstElems = dst.elements;
int newDstSize = dstSize;
// 遍历Link中的元素,然后将其放到Stack的elements中
for (int i = srcStart; i < srcEnd; i++) {
DefaultHandle element = srcElems[i];
// 代表已经被完全回收了,如果他们两个不一样,说明该element被其他回收线程回收了,还没有最终回到创建线程;(也就是还不能使用,需要这样一次迁移)
if (element.recycleId == 0) {
element.recycleId = element.lastRecycledId;
} else if (element.recycleId != element.lastRecycledId) {
throw new IllegalStateException("recycled already");
}
srcElems[i] = null;
// 这里还有一次判断,因为可能会有大量的对象会创建;
// 所以默认1/8的比例进行缓存对象;
if (dst.dropHandle(element)) {
// Drop the object. 如果被丢弃了,就需要继续遍历
continue;
}
// 将element中的stack指针指向创建线程的stack
element.stack = dst;
// stack中的元素++
dstElems[newDstSize ++] = element;
}
// 如果刚刚好达到一个LINK的最大值,也就是占满了一个LINK,而且这个LINK刚刚被回收掉了,那么这个LINK就可以被回收掉;
if (srcEnd == LINK_CAPACITY && head.next != null) {
// Add capacity back as the Link is GCed.
this.head.reclaimSpace(LINK_CAPACITY);
this.head.link = head.next;
}
// 将readIndex指向移动完成的元素位置;
head.readIndex = srcEnd;
// 如果移动完成以后,新老的stack的elements队列的长度是一致的,说明没有可重用对象,返回false
if (dst.size == newDstSize) {
return false;
}
dst.size = newDstSize;
return true;
} else {
// The destination stack is full already.
return false;
}
}
问题集
为什么head和cursor两个变量合二为一,每次从head开始遍历?
- head是一直在移动的,每多一个线程,head就会往前移动一次;从head开始扫描,可能造成对象使用不均匀,WeakOrderQueue链表后面的被回收的元素无法被使用到,那对应的回收线程回收对象的数量一旦达到阈值,一旦在有对象被release,但是因为达到了回收阈值,所以这些对象都会被丢弃;这样会导致有些线程无法被使用,资源利用不均匀;
- 但从cursor继续往下扫描有一个问题点,就是cursor一直扫描到最后,发现没有元素了,而不会从head在扫描一遍,就会直接返回false;那此次就无法重利用;就会导致从(head, cursor)区间内新回收的对象就无法得到重用;我理解设计者这样做的考虑是:利用一次无利用(一次创建对象),来避免从head的多次扫描,从性能、代码复杂度做了均衡考虑
五、高版本(4.1.112)Recycler的变化
变化一:整体架构设计,结构更加简洁
- 之前用Stack、WeakOrderQueue来实现并发、串行无锁化
- 现在变成了使用MpscQueue完成并发串行无锁化的设计;
- 为了和以前接口兼容,所以需要保留之前的DefaultHandle等接口
变化二:封装更加合理
- 语义更加清晰,通过开放ObjectPool来表示当前对象是放在一个对象池中
- 将Recycler进一步封装
- 同时又封装一层Handle,且不失兼容性;