我们知道我们最终拿到的是ByteBuf类,分配的是byte[]数组,byte[]确实是池化的,但是每次申请一个都要去创建一个ByteBuf类,不如把ByteBuf也池化,那么就是个对象池了
Netty的对象池不仅仅针对的ByteBuf,是一个通用的类1
2
3public 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
19class 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
6private static Recycler<Student> studentRecycler = new Recycler<Student>() {
protected Student newObject(Handle<Student> handle) {
return new Student(handle);
}
};
重写newObject
方法,我们直接new
一个就好
下面进行测试1
2
3
4
5
6public static void main(String[] args) {
Student student = studentRecycler.get();
System.out.println(student.getId());
student.recycle();
System.out.println(studentRecycler.get().getId());
}
运行一下可以看到输出结果1
20
0
说明是同一个对象
但是这只是简单的测试
思路
其实对象池,思路并不难,需要的时候new
一个,归还的时候我们保存到一个集合中,再取的时候优先从集合中取。
但是这会催生一些问题
首先最容易想到的就是多线程问题
多线程并发的具体有两个思路
- 加锁,比如使用支持并发的集合类
ThreadLocal
加锁肯定是要消耗性能的,即使是ConcurrentHashMap
这种设计优雅的,还是进行了加锁
但是优点是设计简单
ThreadLocal可以不需要进行加锁,但是相应的就会催生更多的问题,编码也更复杂
什么更多的问题呢
比如
在Thread1
中new
了一个对象,然后在Thread2
中归还了。
如果不加以设计,那么这个对象应该是归还到Thread2
中了。
如果Thread1
疯狂new
对象,全部到Thread2
中归还呢?
那Thread1
的对象池就毫无用处,Thread2
中塞满了对象Thread2
中的对象肯定是要释放的,不然会内存泄漏的
那么就需要对Thread2
的对象池的大小进行规定,同时设置多久没使用就释放的策略
而且这种问题还是很常见的,一般代码逻辑定了,这个就定了
要代码去兼容这个问题肯定不现实
好,那么Thread1的对象,在Thread2中归还了,最终还是要回到Thread1中。
问题也好解决,我们把每个对象池记录下所在的Thread,建立一个\<pool, Thread\>
的Map,归还的时候加以判定
那么又有多线程的问题了。
而且锁很难避免,需要一个良好的设计,尽可能的减少锁。
综上所看,使用ThreadLocal确实优点很多,但是设计上需要考虑很多东西
下面我们看Netty是怎么一一解决这个问题的
设计
首先看看最简单的设计
我们采用ThreadLocal,为每个线程分配一个类似于Stack的结构,Stack内部使用链表或者数组都是可以的。
当有请求过来,从Stack中pop出一个对象,使用完之后再push进去
上文提到,上面一个是有问题的
那么怎么办呢,Netty为每个线程又设计了一个Queue,同时维护每个线程和Queue对应的Map,当其他的线程回收对象的时候,如果发现这个对象不属于自己的线程
那么就放到Onwer线程的Queue中
当线程的Stack进行pop时,如果发现Stack中空了,那么先不执行New一个对象的操作,而是先去对应的Queue中去查看是否为空,如果不为空,那么就从Queue中transfer到Stack中
那么问题似乎解决了 这样是行得通的
但是再细想,其他线程之间的release操作,其他线程release和owner线程的transder操作,似乎有那么点互斥的意思在里面
那么这里面已经怎么设计才能是最佳的呢
首先解决多个other线程的release可能存在的race condition问题,这个也好解决,还是ThreadLocal,对于每一个Queue,对其他的每个Other线程建立一个自己的Queue
这样每个other线程进行release不属于自己的对象的时候,不会产生竞态条件
下面解决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
6class List {
Ele[] ele = new Ele[CAPCITY];
int readIndex;
int writeIndex;
List next;
}
每个节点中,放入一个对象的数组,同时维护readIndex和WriteIndex。
当满了之后,再New一个List,把当前的Next指针指向新的List
总结
终于讲完了
还是学到了很多东西
代码就不带你们去读了,有了思路之后再去读会容易点