从Netty看对象池实现

我们知道我们最终拿到的是ByteBuf类,分配的是byte[]数组,byte[]确实是池化的,但是每次申请一个都要去创建一个ByteBuf类,不如把ByteBuf也池化,那么就是个对象池了

Netty的对象池不仅仅针对的ByteBuf,是一个通用的类

1
2
3
public abstract class Recycler<T> {
protected abstract T newObject(Handle<T> handle);
}

使用

讲一讲正确的使用姿势

首先定义自己的对象,要想实现对象池的功能,对象需要接受一个参数Recycler.Handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student {
private static final AtomicLong ATOMIC_LONG = new AtomicLong();

private Long id;
private Recycler.Handle<Student> handle;

public Student(Recycler.Handle<Student> handle) {
this.id = ATOMIC_LONG.getAndIncrement();
this.handle = handle;
}

public Long getId() {
return id;
}

public void recycle() {
handle.recycle(this);
}
}

这里,我们定义了一个学生的类,同时接受Recycler.Handle的参数
同时定义一个recycle的方法
用处是当我们不用这个方法的时候,调用recycle方法把对象归还到对象池
内容比较单一
就写handle.recycle(this)就行

下面定义我们的Recycler类

1
2
3
4
5
6
private static Recycler<Student> studentRecycler = new Recycler<Student>() {
@Override
protected Student newObject(Handle<Student> handle) {
return new Student(handle);
}
};

重写newObject方法,我们直接new一个就好

下面进行测试

1
2
3
4
5
6
public static void main(String[] args) {
Student student = studentRecycler.get();
System.out.println(student.getId());
student.recycle();
System.out.println(studentRecycler.get().getId());
}

运行一下可以看到输出结果

1
2
0
0

说明是同一个对象

但是这只是简单的测试

思路

其实对象池,思路并不难,需要的时候new一个,归还的时候我们保存到一个集合中,再取的时候优先从集合中取。

但是这会催生一些问题
首先最容易想到的就是多线程问题

多线程并发的具体有两个思路

  • 加锁,比如使用支持并发的集合类
  • ThreadLocal

加锁肯定是要消耗性能的,即使是ConcurrentHashMap这种设计优雅的,还是进行了加锁
但是优点是设计简单

ThreadLocal可以不需要进行加锁,但是相应的就会催生更多的问题,编码也更复杂

什么更多的问题呢
比如
Thread1new了一个对象,然后在Thread2中归还了。
如果不加以设计,那么这个对象应该是归还到Thread2中了。

如果Thread1疯狂new对象,全部到Thread2中归还呢?
Thread1的对象池就毫无用处,Thread2中塞满了对象
Thread2中的对象肯定是要释放的,不然会内存泄漏的
那么就需要对Thread2的对象池的大小进行规定,同时设置多久没使用就释放的策略

而且这种问题还是很常见的,一般代码逻辑定了,这个就定了
要代码去兼容这个问题肯定不现实

好,那么Thread1的对象,在Thread2中归还了,最终还是要回到Thread1中。
问题也好解决,我们把每个对象池记录下所在的Thread,建立一个\<pool, Thread\>的Map,归还的时候加以判定
那么又有多线程的问题了。
而且锁很难避免,需要一个良好的设计,尽可能的减少锁。

综上所看,使用ThreadLocal确实优点很多,但是设计上需要考虑很多东西

下面我们看Netty是怎么一一解决这个问题的

设计

简单的对象池
首先看看最简单的设计
我们采用ThreadLocal,为每个线程分配一个类似于Stack的结构,Stack内部使用链表或者数组都是可以的。
当有请求过来,从Stack中pop出一个对象,使用完之后再push进去



2
上文提到,上面一个是有问题的
那么怎么办呢,Netty为每个线程又设计了一个Queue,同时维护每个线程和Queue对应的Map,当其他的线程回收对象的时候,如果发现这个对象不属于自己的线程
那么就放到Onwer线程的Queue中
当线程的Stack进行pop时,如果发现Stack中空了,那么先不执行New一个对象的操作,而是先去对应的Queue中去查看是否为空,如果不为空,那么就从Queue中transfer到Stack中

那么问题似乎解决了 这样是行得通的
但是再细想,其他线程之间的release操作,其他线程release和owner线程的transder操作,似乎有那么点互斥的意思在里面

那么这里面已经怎么设计才能是最佳的呢



3

首先解决多个other线程的release可能存在的race condition问题,这个也好解决,还是ThreadLocal,对于每一个Queue,对其他的每个Other线程建立一个自己的Queue

这样每个other线程进行release不属于自己的对象的时候,不会产生竞态条件



4

下面解决other线程的release和owner线程的transfer之间的同步问题
其实他们之间的问题并不是互斥的问题,而是同步的问题,而且这个同步也并没有涉及notify,wait之类的操作

不管是什么容器,我们维护readIndex和writeIndex这两个指针就行,进行transfer的时候,直接记录下writeIndex,就transfer到这个位置就好

那么底层使用什么进行维护呢,这个还是可以讲究一下的
我们设想几个方案

  • 一个大数组 可行吗?感觉不是太好,首先会有扩容和缩容操作,其次writeIndex和readIndex一直往前走,那么小于readIndex的那部分其实不太好管理,容易产生浪费
  • 链表 可行吗?似乎是可行的 但是对于每一个对象,都会产生一个与之伴随的Entry对象,而且这个Entry可能会较多,都是小对象,对GC不是那么友好。除非能池化。想什么呢,还玩递归?呸!

似乎陷入了僵局

再想想,能不能兼容两者的优点
记得Redis中的QuickList吗

对的,Netty中就是这么设计的

1
2
3
4
5
6
class List {
Ele[] ele = new Ele[CAPCITY];
int readIndex;
int writeIndex;
List next;
}

每个节点中,放入一个对象的数组,同时维护readIndex和WriteIndex。
当满了之后,再New一个List,把当前的Next指针指向新的List

总结

终于讲完了
还是学到了很多东西
代码就不带你们去读了,有了思路之后再去读会容易点