前言

池化思想在实际开发中有很多应用,指的是针对一些创建成本高,创建频繁的对象,用完不弃,将其缓存在对象池子里,下次使用时优先从池子里获取,如果获取到则可以直接使用,以此降低创建对象的开销。

我们最熟悉的数据库连接池就是一种池化思想的应用,数据库操作是非常频繁的,数据库连接的创建、销毁开销很大,每次都需要进行TCP三次握手和四次挥手,权限检查等,所以如果每次操作数据库都重新创建连接,用完就丢弃,对于应用程序来说是不可接受的。在java世界里,一切皆对象,所以需要有一个数据库对象连接池,用于保存连接池对象。例如使用hikari,可以配置spring.datasource.hikari.maximum-pool-size=20,表示最多可以池化20个数据库连接对象。

此外,频繁的创建销毁对象还会影响GC,当一个对象使用完,再没被GC root引用,就变成不可达,所引用的内存可以被垃圾回收,GC是需要STW的,频繁的GC也会影响程序的吞吐量。

本篇我们要介绍的是netty的对象池Recycler,Recycler是对象池核心类,netty为了减少依赖,以及追求高性能,并没有使用第三方的对象池,而是自己设计了一套。

netty在高并发处理IO读写,内存对象的使用是非常频繁的,如果每次都重新申请,无疑性能会大打折扣,特别是对于堆外内存,申请和销毁的成本更高,所以对内存对象使用池化是很有必要的。

例如:PooledHeapByteBuf,PooledDirectByteBuf,ChannelOutboundBuffer.Entry都使用了对象池,这些类内部都有一个Recycler静态变量和一个Handle实例变量。

static final class Entry {
private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
@Override
protected Entry newObject(Handle<Entry> handle) {
return new Entry(handle);
}
}; private final Handle<Entry> handle;
}

原理

我们先通过一个例子感受一下Recycler的使用,然后再来分析它的原理。

public final class Connection {

	private Recycler.Handle handle;

	private Connection(Recycler.Handle handle) {
this.handle = handle;
} private static final Recycler<Connection> RECYCLER = new Recycler<Connection>() {
@Override
protected Connection newObject(Handle<Connection> handle) {
return new Connection(handle);
}
}; public static Connection newInstance() {
return RECYCLER.get();
} public void recycle() {
handle.recycle(this);
} public static void main(String[] args) {
Connection c1 = Connection.newInstance();
int hc1 = c1.hashCode();
c1.recycle();
Connection c2 = Connection.newInstance();
int hc2 = c2.hashCode();
c2.recycle();
System.out.println(hc1 == hc2); //true
}
}

代码非常简单,我们用final修饰Connection,这样就无法通过继承创建对象。同时构造方法定义为私有,防止外部直接new创建对象,这样就只能通过newInstance静态方法创建对象。

Recycler是一个抽象类,newObject是它的抽象方法,这里使用匿名类继承Recycler并重写newObject,用于创建一个新的对象。

Handle是一个接口,Recycler会创建并通过newObject方法传进来,默认是DefaultHandle,它的作用是用来回收对象,放回对象池。

接着我们创建两个Connection实例,可以看到它们的hashcode是一样的,证明是同一个对象。

需要注意的是,使用对象池创建的对象,用完需要调用recycle回收。

原理分析

想象一下,如果由我们设计,怎么设计一个高性能的对象池呢?对象池的操作很简单,一取一放,但考虑到多线程,实际情况就变得复杂了。

如果只有一个全局的对象池,多线程操作需要保证线程安全,那就需要通过加锁或者CAS,这都会影响存取效率,由于线程竞争,锁等待,可能通过对象池获取对象的效率还不如直接new一个,这样就得不偿失了。

针对这种情况,已经有很多的经验供我们借鉴,核心思想都是一样的,降低锁竞争。例如ConcurrentHashMap,通过每个节点上锁,hash到不同节点的线程就不会相互竞争;例如ThreadLocal,通过在线程级别绑定一个ThreadLocalMap,每个线程操作的都是自己的私有变量,不会相互竞争;再比如jvm在分配内存的时候,内存区域是共享的,所以jvm为每个线程设计了一块私有的TLAB,可以高效进行内存分配,关于TLAB可以参考:这篇文章

这种无锁化的设计在netty中非常常见,例如对象池,内存分配,netty还设计了FastThreadLocal来代替jdk的ThreadLocal,使得线程内的存取更加高效。

Recycler设计如下:

如上图,Recycler内部维护了两个重要的变量,StackWeakOrderQueue,实际对象就是包装成DefaultHandle,保存在这两个结构中。

默认情况一个线程最多存储4 * 1024个对象,可以根据实际情况,通过Recycler的构造函数指定。

private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.

Stack是一个栈结构,是线程私有的,Recycler内部通过FastThreadLocal进行定义,对Stack的操作不会有线程安全问题。

 private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {};

FastThreadLocal是netty版的ThreadLocal,搭配FastThreadLocalThread,FastThreadLocalMap使用,主要优化jdk ThreadLocal扩容需要rehash,和hash冲突问题。

当获取对象时,就是尝试从Stack栈顶pop出一个对象,如果有,则直接使用。如果没有就尝试从WeakOrderQueue“借”一点过来,放到Stack,如果借不到,那就调用newObject()创建一个。

WeakOrderQueue主要是用来解决多线程问题的,考虑这种情况,线程A创建的对象,可能被线程B使用,那么对象的释放就应该由线程B决定。如果线程B也将对象归还到线程A的Stack,那就出现了线程安全问题,线程A对Stack的读取,写入就需要加锁,影响并发效率。

为了无锁化操作,netty为其它每个线程都设计了一个WeakOrderQueue,各个线程只会操作自己的WeakOrderQueue,不会有并发问题了。其它线程的WeakOrderQueue会通过指针构成一个链表,Stack对象内部通过3个指针指向链表,这样就可以遍历整个链表对象。

站在线程A的角度,其它线程就是B,C,D...,站在线程B的角度,其它线程就是A,C,D...

从上图可以看到,WeakOrderQueue实际不是一个队列,内部是由一些Link对象构成的双向链表,它也是一个链表。

Link对象是一个包含读写索引,和一个长度为16的数组的对象,数组存储的就是DefaultHandler对象。

整个过程是这样的,当本线程从Stack获取不到可用对象时,就会通过cursor指针变量WeakOrderQueue链表,开始从其它线程获取对象。如果找到一个可用的Link,就会将整个Link里的对象迁移到Stack,然后删除链表节点,为了保证效率,每次最多迁移一个Link。如果还获取不到,就通过newObject()方法创建一个新的对象。

Recycler#get 方法如下:

 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;
}

pop方法判断Stack没有对象,就会调用scavenge方法,从WeakOrderQueue迁移对象。scavenge,翻译过来是拾荒,捡的意思。

 DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
if (!scavenge()) {
return null;
}
size = this.size;
}
//...
}

最终会调用到WeakOrderQueue的transfer方法,这个方法比较复杂,主要是对WeakOrderQueue链表和内部Link链表的遍历。

这里dst就是前面说的Stack对象,可以看到会把element元素迁移过去。

boolean transfer(Stack<?> dst) {
//...
if (srcStart != srcEnd) {
final DefaultHandle[] srcElems = head.elements;
final DefaultHandle[] dstElems = dst.elements;
int newDstSize = dstSize;
for (int i = srcStart; i < srcEnd; i++) {
DefaultHandle element = srcElems[i];
if (element.recycleId == 0) {
element.recycleId = element.lastRecycledId;
} else if (element.recycleId != element.lastRecycledId) {
throw new IllegalStateException("recycled already");
}
srcElems[i] = null; if (dst.dropHandle(element)) {
// Drop the object.
continue;
}
element.stack = dst;
dstElems[newDstSize ++] = element;
}
}
//...
}

应用

我们项目使用了mybatis plus作为orm,其中用得最多的就是QueryWrapper了,每次查询都需要new一个QueryWrapper。例如:

QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper.eq("uid", 123);
return userMapper.selectOne(queryWrapper);

数据库查询是非常频繁的,QueryWrapper的创建虽然不会很耗时,但过多的对象也会给GC带来压力。

QueryWrapper是mp提供的类,它没有池化的实现,不过我们可以参考上面netty DefaultHandle的思路,在它外面再包一层,然后池化包装后的对象。

回收的时候还要注意清空对象的属性,例如上面给uid赋值了123,下个对象就不能用这个条件,否则就乱套了,QueryWrapper提供了clear方法可以重置所有属性。

同时,每次用完都需要手动recycle也是比较麻烦的,开发容易忘记,可以借助AutoCloseable接口,使用try-with-resource的写法,在结束后自动完成回收。

对于修改和删除还有UpdateWrapper和DeleteWrapper,同样思路也可以实现。

有了这些思路,代码就出来了:

public final class WrapperUtils {

	private WrapperUtils() {}

	private static final Recycler<PooledQueryWrapper> QUERY_WRAPPER_RECYCLER = new Recycler<PooledQueryWrapper>() {
@Override
protected PooledQueryWrapper newObject(Handle<PooledQueryWrapper> handle) {
return new PooledQueryWrapper<>(handle);
}
}; public static <T> PooledQueryWrapper<T> newInstance() {
return QUERY_WRAPPER_RECYCLER.get();
} static class PooledQueryWrapper<T> implements AutoCloseable { private QueryWrapper<T> queryWrapper;
private Recycler.Handle<PooledQueryWrapper> handle; public PooledQueryWrapper(Recycler.Handle<PooledQueryWrapper> handle) {
this.queryWrapper = new QueryWrapper<>();
this.handle = handle;
} public QueryWrapper<T> getWrapper() {
return this.queryWrapper;
} @Override
public void close() {
queryWrapper.clear();
handle.recycle(this);
}
}
}

使用如下,可以看到打印出来的hashcode都是一样的,每次执行后都会自动调用close方法,进行QueryWrapper属性重置。

public static void main(String[] args) {
try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 1);
wrapper.select("id,name");
wrapper.last("limit 1");
System.out.println(wrapper.hashCode());
} try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 2);
wrapper.select("id,email");
wrapper.last("limit 2");
System.out.println(wrapper.hashCode());
} try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 3);
wrapper.select("id,phone");
wrapper.last("limit 3");
System.out.println(wrapper.hashCode());
}
}

总结

之前我们也分析过apache common pool,这也是一个池化实现,在redis客户端也有应用,但它是通过加锁解决并发问题的,设计没有netty这么精细。

上面的源码来自netty4.1.42,从整体上看整个Recycler的设计还是比较复杂的,主要为了解决多线程竞争和GC问题,导致整个代码复杂度比较高,所以netty在后来的版本中对其进行重构。

不过这不影响我们对它思想的学习,以后也可以借鉴到实际开发中。

更多分享,欢迎关注我的github:https://github.com/jmilktea/jtea

netty Recycler对象池的更多相关文章

  1. 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...

  2. netty源码分析 - Recycler 对象池的设计

    目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...

  3. Netty轻量级对象池实现分析

    什么是对象池技术?对象池应用在哪些地方? 对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念.对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数.池 ...

  4. Netty 高性能之道 - Recycler 对象池的复用

    前言 我们知道,Java 创建一个实例的消耗是不小的,如果没有使用栈上分配和 TLAB,那么就需要使用 CAS 在堆中创建对象.所以现在很多框架都使用对象池.Netty 也不例外,通过重用对象,能够避 ...

  5. 基于Netty包中的Recycler实现的对象池技术详解

    一.业务背景 当项目中涉及到频繁的对象的创建和回收的时候,就会出现频繁GC的情况,这时就出现了池化的技术来实现对象的循环使用从而避免对象的频繁回收,Netty包下的Recycler就实现了这一功能.当 ...

  6. Netty源码解析 -- 对象池Recycler实现原理

    由于在Java中创建一个实例的消耗不小,很多框架为了提高性能都使用对象池,Netty也不例外. 本文主要分析Netty对象池Recycler的实现原理. 源码分析基于Netty 4.1.52 缓存对象 ...

  7. java对象池化技术

    https://blog.csdn.net/tiane5hao/article/details/85957840 文章目录 先写一个简单通用的对象池 通过上面的通用池实现jedis连接池 连接池测试 ...

  8. 基于Apache组件,分析对象池原理

    池塘里养:Object: 一.设计与原理 1.基础案例 首先看一个基于common-pool2对象池组件的应用案例,主要有工厂类.对象池.对象三个核心角色,以及池化对象的使用流程: import or ...

  9. 设计模式之美:Object Pool(对象池)

    索引 意图 结构 参与者 适用性 效果 相关模式 实现 实现方式(一):实现 DatabaseConnectionPool 类. 实现方式(二):使用对象构造方法和预分配方式实现 ObjectPool ...

  10. Egret中的对象池ObjectPool

    为了可以让对象复用,防止大量重复创建对象,导致资源浪费,使用对象池来管理. 对象池具体含义作用,自行百度. 一 对象池A 二 对象池B 三 字符串key和对象key的效率 一 对象池A /** * 对 ...

随机推荐

  1. Pandas日期时间格式化

    当进行数据分析时,我们会遇到很多带有日期.时间格式的数据集,在处理这些数据集时,可能会遇到日期格式不统一的问题,此时就需要对日期时间做统一的格式化处理.比如"Wednesday, June ...

  2. Laravel入坑指南(12)——最终章:Session、缓存与Redis

    因为web服务除了业务准确之外,我们最关注的就是服务的性能.鉴于web服务几乎都是IO密集型,我们为了提高IO的速度,自然不能把所有的数据都放在关系型数据库中.而redis的并发与性能可以很好地帮我们 ...

  3. Oracle 表压缩(Table Compression)技术介绍

    Oracle 表压缩(Table Compression)介绍 1.官方文档说法: As your database grows in size, consider using table compr ...

  4. Springboot集成Disruptor做内部消息队列

    一.基本介绍 Disruptor的github主页:https://github.com/LMAX-Exchange/disruptor 1,什么是 Disruptor? (1)Disruptor 是 ...

  5. 异常处理之raise A from B

    raise A from B 语句用于连锁chain异常 from 后面的B可以是: - 异常类 - 异常实例 - None 如果B是异常类或者异常实例,那么B会被设置为A的__cause__属性,表 ...

  6. 鸿蒙开发学习(二)之ArkUI

    目录 UI开发 布局 布局选择 布局位置 组件 容器组件 row.column RelativeContainer 列表 Tabs 子组件 页面路由 GitHub地址,欢迎star HmDemo: 鸿 ...

  7. 框架和MVC架构

    网络框架及MVC架构 网络框架 所谓网络框架是指这样的一组Python包,它能够使开发者专注于网站应用业务逻辑的开发,而无须处理网络应用底层的协议.线程.进程等方面.这样能大大提高开发者的工作效率,同 ...

  8. 【Azure Function App】Java Function在运行中遇见内存不足的错误

    问题描述 在Function的Code+Test界面进行函数触发可以成功.因为Function为Blob Trigger,当在Blob容器下上传文件后,Function可以被正常触发但是报 outof ...

  9. 【Azure Storage Blob】如何通过.NET Azure Storage Blobs SDK获取到Blob的MD5值呢?

    问题描述 通过.NET Azure Storage Blobs SDK , 获取Blob的MD5值,查看了Azure操作手册中,介绍可以使用 blob.Properties.ContentMD5 属性 ...

  10. 【Azure 应用服务】Azure Function 中运行Powershell 脚本,定位 -DefaultProfile 引发的错误

    问题描述 突然之间,使用PowerShell脚本 Get-AzVirtualNetwork 获取虚拟网络信息时,如果带上  -DefaultProfile $sub 参数,就出现 Azure cred ...