netty源码解析六之Recycler对象池

六:Recycler对象池

1.对象池Recycler的使用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
    //创建对象池
    private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
            new ObjectCreator<PooledDirectByteBuf>() {
        @Override
        public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    });

    static PooledDirectByteBuf newInstance(int maxCapacity) {
        //从对象池中获取对象
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }
abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
    //对象在对象池中的回收句柄
    private final Handle<PooledByteBuf<T>> recyclerHandle;
    private void recycle() {
        //回收对象
        recyclerHandle.recycle(this);
    }
  
}

netty中需要大量创建PooledDirectByteBuf对象,为了避免高并发场景频发创建对象的开销,从而引入了对象池来统一管理PooledDirectByteBuf对象。

Netty中每个被池化的对象中都会引用对象池的实例ObjectPool RECYCLER ,这个对象池的实例就是专门用来分配和管理被池化对象的。

这里RECYCLER对象池是专门用来管理PooledDirectByteBuf对象的,通过泛型指定需要管理的具体对象。

泛型类ObjectPool是netty为对象池设计的顶级抽象。对象池的行为均定义在这个泛型抽象类中。可通过ObjectPool#newPool方法创建指定的对象池。其参数ObjectCreator creator接口用于定义创建池化对象的行为。当对象需要创建对象时,通过ObjectCreator.newObject来创建对象。

每个池化对象中都会包含recyclerHandle(池化对象在对象池中的句柄),里边封装了和对象池相关的一些行为和信息,recyclerHandle是由对象池在创建对象后传递进来的。

RECYCLER.get():从PooledDirectByteBuf对象池中获取对象即可。

recyclerHandle.recycle(this) :把对象回收到对象池中。

4.2 对象池在Channel写入缓冲队列中的使用

每个Channel都会有一个独立的写入缓冲队列ChannelOutboundBuffer,用来暂时存储用户的待发送数据。这样用户可以在调用channel的write方法之后立马返回,实现异步发送流程。

在发送数据时,Channel首先会将用户要发送的数据缓存在自己的写缓存队列ChannelOutboundBuffer中。而ChannelOutboundBuffer中的元素类型为Entry。在Netty中会大量频繁的创建Entry对象。所以Entry对象同样也需要被对象池管理起来。

static final class Entry {
    //Entry的对象池,用来创建和回收Entry对象
    //静态变量引用类型地址 这个是在Klass Point(类型指针)中定义 8字节(开启指针压缩 为4字节)
    private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() {
        @Override
        public Entry newObject(Handle<Entry> handle) {
            return new Entry(handle);
        }
    });

    //recyclerHandle用于回收对象
    private final Handle<Entry> handle;
  
  //Entry对象只能通过对象池获取,不可外部自行创建
    private Entry(Handle<Entry> handle) {
    			this.handle = handle;
     }
 
}

在对象池创建对象时,会为池化对象创建其在对象池中的句柄Handler,随后将Handler传入创建好的池化对象中。当对象使用完毕后,我们可以通过Handler来将对象回收至对象池中等待下次继续使用。

从对象池中获取对象

由于Entry对象在设计上是被对象池管理的,所以不能对外提供public构造函数,无法在外面直接创建Entry对象。

所以池化对象都会提供一个获取对象实例的 static 方法 newInstance。在该方法中通过RECYCLER.get()从对象池中获取对象实例。

static Entry newInstance(Object msg, int size, long total, ChannelPromise promise) {
    //从对象池获取对象
    Entry entry = RECYCLER.get();
......
    return entry;
}

使用完毕回收对象

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

2.Recycler总体设计

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5.1 多线程获取对象无锁化设计

每个线程都从对象池中获取对象,会产生并发问题。

  • 这里采用TLAB的思想,为每个线程单独创建stack对象,每次获取对象从stack栈顶弹出对象获取,归还对象时入栈。避免多线程并发申请内存时的同步锁定开销

5.2 Stack的设计

Stack的设计主要分为两个重要的部分:

  • 数组实现的栈结构用来存放对象池中的对象,每个线程绑定一个独立的Stack用来存储由该线程创建出来并回收到对象池中的对象。
  • WeakOrderQueue链表,head 指针指向WeakOrderQueue链表的头结点,cursor 指针指向链表的当前节点,prev 指针指向当前节点的前一个节点。WeakOrderQueue链表是用来存储其他线程帮助本线程回收的对象(我们称之为待回收对象)。其中WeakOrderQueue链表中的每一个节点对应一个其他线程,这个其他线程为本线程回收的对象存储在对应的WeakOrderQueue节点中。

Stack结构在设计上为什么要引入这个WeakOrderQueue链表呢

假设Stack结构中只有一个数组栈,并没有WeakOrderQueue链表。看看这样会产生什么后果?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果这是单线程处理业务场景,这里thread1从栈中获取对象,然后对象会在thread1处理完业务后归还给栈中。当thread1需要再次获取对象时,从stack1中直接获取上次回收的对象。

如果这是一个多线程处理业务场景,thread1获取的对象可能会交给thread2~4处理之后的业务。当业务执行结束,归还的操作可能会由其他线程执行。如果多个线程并发向stack1释放回收对象,会出现同步竞争。如果thread1同时触发了获取对象操作,将不得不等待其他线程释放对象到stack1中。

引入对象池是为了减少频繁创建对象开销以及gc停顿,这里有引入了同步的开销,这里得不偿失。

无锁化设计思想

这里为每个线程(非stack1所持有的thread1)分配WeakOrderQueue节点,每个线程回收对象时会将这些对象暂时存放自己对应的WeakOrderQueue节点中。这样就避免了多线程回收场景中的同步竞争。当所有线程为stack1回收对象时,在stack1中就形成了WeakOrderQueue链表。

当thread1从自己对应的stack1中获取对象时,先从stack1中获取,因为单线程不存在竞争。获取不到则根据cursor指针遍历Stack结构中的WeakOrderQueue链表,将当前WeakOrderQueue节点存放的待回收对象转移至数组栈中。如果WeakOrderQueue链表中也没有任何待回收对象可以转移。那么创建线程在对象池中就直接创建一个对象出来返回。

5.3 WeakOrderQueue的设计

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里可以看出WeakOrderQueue是一个链表结构,类型为Link,head和tail分别存储头部和尾部对象。

Link类型中包含elements数组,该数组存储线程收集的待回收对象。readIndex表示elements数组的读取位置。writeIndex表示数组写入位置。elements数组容量默认16。当线程回收对象超过16个时,会重新创建一个Link节点插入到Link链表尾部。

当需要将WeakoOrderQueue节点中所存放的待回收对象回收转移至其对应的Stack结构中的数组栈中时,创建线程会遍历当前WeakOrderQueue节点中的Link链表,然后从链表的Head节点开始,将Head节点中包裹的Link链表头结点中存放的待回收对象回收至创建线程对应的Stack中。一次最多转移一个Link大小的待回收对象(16个)。

当Link节点中的待回收对象全部转移至创建线程对应的Stack中时,会立马将这个Link节点从当前WeakOrderQueue节点中的Link链表里删除,随后Head节点向后移动指向下一个Link节点。

head指针始终指向**第一个未被转移完毕的Link节点**,创建线程从head节点处读取转移待回收对象,回收线程从Tail节点处插入待回收对象。**这样转移操作和插入操作互不影响、没有同步的开销**。

这里会存在线程可见性的问题,也就是说回收线程刚插入的待回收对象,在创建线程转移这些待回收对象时,创建线程可能会看不到由回收线程刚刚插入的待回收对象。

Netty这里为了不引入多线程同步的开销,只会保证待回收对象的最终可见性。因为如果要保证待回收对象的实时可见性,就要插入一些内存屏障指令,执行这些内存屏障指令也是需要开销的。

事实上这里也并不需要保证实时可见性,创建线程暂时看不到WeakOrderQueue节点中的待回收对象也是没关系的,大不了就新创建一个对象。这里还是遵循无锁化的设计思想

6.Recycler对象池的实现

6.1 对象池的顶层设计ObjectPool

public abstract class ObjectPool<T> {

    ObjectPool() { }

    public abstract T get();

    public interface Handle<T> {
        void recycle(T self);
    }

    public interface ObjectCreator<T> {
        T newObject(Handle<T> handle);
    }

    public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) {
        return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator"));
    }

    private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
        //recycler对象池实例
        private final Recycler<T> recycler;

        RecyclerObjectPool(final ObjectCreator<T> creator) {
             recycler = new Recycler<T>() {
                @Override
                protected T newObject(Handle<T> handle) {
                    return creator.newObject(handle);
                }
            };
        }

        @Override
        public T get() {
            return recycler.get();
        }
    }
}

Handle是池化对象在对象池中的一个模型,Handle里面包裹了池化对象,并包含了池化对象的一些回收信息,以及池化对象的回收状态。它的默认实现是DefaultHandle.

为什么要将池化对象的回收行为recycle定义在Handle中,而不是ObejctPool中呢

站在业务的角度,业务线程处理的都是对象级别这个维度,并不需要知道对象池的存在。所以使用完对象后,调用对象的回收方法recycle将池化对象回收掉即可。

abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
      //对象在对象池中的回收句柄
    private final Handle<PooledByteBuf<T>> recyclerHandle;
  
      protected PooledByteBuf(Handle<? extends PooledByteBuf<T>> recyclerHandle, int maxCapacity) {
        super(maxCapacity);
        this.recyclerHandle = (Handle<PooledByteBuf<T>>) recyclerHandle;
    }
      private void recycle() {
        //回收对象
        recyclerHandle.recycle(this);
    }
  
}

这里对象的recycle方法实际上是调用handle的recycle方法进行回收。handle对象是ObjectCreator#newObject方法调用时传递的。

private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
        new ObjectCreator<PooledDirectByteBuf>() {
    @Override
    public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
        return new PooledDirectByteBuf(handle, 0);
    }
});
6.1.1 创建ObjectPool

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public abstract class ObjectPool<T> {   
		public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) {
        return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator"));
    }

    private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
        //recycler对象池实例
        private final Recycler<T> recycler;

        RecyclerObjectPool(final ObjectCreator<T> creator) {
             recycler = new Recycler<T>() {
                @Override
                protected T newObject(Handle<T> handle) {
                    return creator.newObject(handle);
                }
            };
        }

        @Override
        public T get() {
            return recycler.get();
        }
    }
}
public abstract class Recycler<T> {
  
      protected abstract T newObject(Handle<T> handle);
}

调用 ObjectPool#newPool 创建对象池时,返回的是 RecyclerObjectPool 实例。而真正的对象池 Recycler 被包裹在 RecyclerObjectPool 中。

对象池Recycler创建对象的行为定义在用户在创建对象池时指定的ObjectCreator 中。

7. Recycler对象池属性详解

7.1 创建线程,回收线程的Id标识

public abstract class Recycler<T> {
     //用于产生池化对象中的回收Id,主要用来标识池化对象被哪个线程回收
    private static final AtomicInteger ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
    //用于标识创建池化对象的线程Id 注意这里是static final字段 也就意味着所有的创建线程OWN_THREAD_ID都是相同的
    //这里主要用来区分创建线程与非创建线程。多个非创建线程拥有各自不同的Id
    //这里的视角只是针对池化对象来说的:区分创建它的线程,与其他回收线程
    private static final int OWN_THREAD_ID = ID_GENERATOR.getAndIncrement(); 
  
}

ID_GENERATOR:标识回收线程id。

OWN_THREAD_ID:标识创建线程id,用于区分回收线程。

在多线程场景下创建线程会有很多个(thread1,thread2,thread3…),既然所有的Recycler对象池实例中的OWN_THREAD_ID都是一样的,那如何区分不同创建线程了?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在对象池中我们并不需要区分创建线程之间的id,因为Netty在设计对象池的时候采用了无锁化设计,创建线程与创建线程之间并不需要交互,每个线程只需要关注自己线程内的对象管理工作即可,所以从一个线程的内部视角来看,只会有一个创建线程就是它自己本身,剩下的线程均是回收线程。所以我们对象池的设计中只需要区分创建线程与回收线程就可以了,当然每个回收线程的Id是不一样的。

回收线程的Id是由其对应的 WeakOrderQueue 节点来分配的,一个 WeakOrderQueue 实例对应一个回收线程Id。

private static final class WeakOrderQueue extends WeakReference<Thread> {
          //回收线程回收Id,每个weakOrderQueue分配一个,同一个stack下的一个回收线程对应一个weakOrderQueue节点
        private final int id = ID_GENERATOR.getAndIncrement();
  
}

7.2 对象池中的容量控制

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//对象池中每个线程对应的Stack中可以存储池化对象的默认初始最大个数 默认为4096个对象
private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.
// 对象池中线程对应的Stack可以存储池化对象默认最大个数 4096
private static final int DEFAULT_MAX_CAPACITY_PER_THREAD;
// 定义每个创建线程对应的Stack结构中的数组栈的初始容量 min(DEFAULT_MAX_CAPACITY_PER_THREAD, 256) 初始容量不超过256个
private static final int INITIAL_CAPACITY;

Recycler 对象池中定义了以上三个属性用于控制对象池中可以池化的对象容量。这些属性对应的初始化逻辑如下:

static {
    //对stack数组栈容量限制
    int maxCapacityPerThread = SystemPropertyUtil.getInt("io.netty.recycler.maxCapacityPerThread",
            SystemPropertyUtil.getInt("io.netty.recycler.maxCapacity", DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD));
    if (maxCapacityPerThread < 0) {
        maxCapacityPerThread = DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD;
    }

    DEFAULT_MAX_CAPACITY_PER_THREAD = maxCapacityPerThread;
    //初始化stack容量
    INITIAL_CAPACITY = min(DEFAULT_MAX_CAPACITY_PER_THREAD, 256);
}
  • DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD:定义每个创建线程对应的Stack结构中的数组栈初始默认的最大容量。默认为4096个。
  • DEFAULT_MAX_CAPACITY_PER_THREAD:定义每个创建线程对应的Stack结构中的数组栈的最大容量。可由JVM启动参数 -D io.netty.recycler.maxCapacityPerThread 指定,如无特殊指定,即采用 DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD 的值,默认为4096个。
  • INITIAL_CAPACITY : 定义每个创建线程对应的Stack结构中的数组栈的初始容量。计算公式为min(DEFAULT_MAX_CAPACITY_PER_THREAD, 256),默认为256个。当池化对象超过256个时,则对对象池进行扩容,但不能超过最大容量 DEFAULT_MAX_CAPACITY_PER_THREAD。

7.3 回收线程可回收对象的容量控制

//    总共回收对象总量就是通过对象池的最大容量和该计算因子计算出来的。计算公式:max(maxCapacity  / maxSharedCapacityFactor, LINK_CAPACITY)
    //用于计算回收线程可帮助回收的最大容量因子  默认为2
    private static final int MAX_SHARED_CAPACITY_FACTOR;

    //每个回收线程最多可以帮助多少个创建线程回收对象 默认:cpu核数 * 2
    private static final int MAX_DELAYED_QUEUES_PER_THREAD;

    //回收线程对应的WeakOrderQueue节点中的Link链表中的节点存储待回收对象的容量 默认为16
    private static final int LINK_CAPACITY;

Recycler 对象池除了对创建线程中的 Stack 容量进行限制外,还需要对回收线程可回收对象的容量进行限制。相关回收容量限制属性初始化逻辑如下:

static {
    MAX_SHARED_CAPACITY_FACTOR = max(2,
            SystemPropertyUtil.getInt("io.netty.recycler.maxSharedCapacityFactor",
                    2));

    MAX_DELAYED_QUEUES_PER_THREAD = max(0,
            SystemPropertyUtil.getInt("io.netty.recycler.maxDelayedQueuesPerThread",
                    // We use the same value as default EventLoop number
                    NettyRuntime.availableProcessors() * 2));

    LINK_CAPACITY = safeFindNextPositivePowerOfTwo(
            max(SystemPropertyUtil.getInt("io.netty.recycler.linkCapacity", 16), 16));
}
  • MAX_SHARED_CAPACITY_FACTOR : 针对创建线程中的 Stack,其对应的所有回收线程总共可帮助其回收的对象总量计算因子。默认为2。可通过JVM参数 -D io.netty.recycler.maxSharedCapacityFactor 指定,总共回收对象总量就是通过对象池的最大容量和该计算因子计算出来的。计算公式:max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY) 。由此我们可以知道创建线程对应的所有回收线程总共可帮助其回收的对象总量默认为2048个,最小回收容量为 LINK_CAPACITY 默认为16。
  • MAX_DELAYED_QUEUES_PER_THREAD : 该参数定义每个回收线程最多可帮助多少个创建线程回收对象。默认为:CPU核数 * 2。可通过JVM参数 -D io.netty.recycler.maxDelayedQueuesPerThread 指定。注意:这里是站在回收线程的角度
  • LINK_CAPACITY : 在创建线程对应的 Stack 结构中的 WeakOrderQueue 链表中,回收线程对应的WeakOrderQueue节点中的Link链表中的Link节点存储待回收对象的容量。默认为16,可通过JVM参数 -D io.netty.recycler.linkCapacity 指定。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.4 对象回收频率控制

对象池不能不考虑容量的限制而无脑的进行对象的回收,而是要对回收对象的频率进行限制。

//创建线程回收对象时的回收比例,默认是8,表示只回收1/8的对象。也就是产生8个对象回收一个对象到对象池中
private static final int RATIO;
//回收线程回收对象时的回收比例,默认也是8,同样也是为了避免回收线程回收队列疯狂增长 回收比例也是1/8
private static final int DELAYED_QUEUE_RATIO;

对象回收频率控制参数的初始化逻辑如下:

static {
    RATIO = max(0, SystemPropertyUtil.getInt("io.netty.recycler.ratio", 8));
    DELAYED_QUEUE_RATIO = max(0, SystemPropertyUtil.getInt("io.netty.recycler.delayedQueue.ratio", RATIO));
}

在池化对象被回收的时候分别由两类线程来执行。

  • 一类是创建线程。池化对象在创建线程中被创建出来后,一直在创建线程中被处理,处理完毕后由创建线程直接进行回收。而为了避免对象池不可控制地迅速膨胀,所以需要对创建线程回收对象的频率进行限制。这个回收频率由参数 RATIO 控制,默认为8,可由JVM启动参数 -D io.netty.recycler.ratio 指定。表示创建线程只回收 1 / 8 的对象,也就是每创建 8 个对象最后只回收 1个对象。
  • 另一类就是回收线程。池化对象在创建线程中被创建出来,但是业务的相关处理是在回收线程中,业务处理完毕后由回收线程负责回收。前边提到对象回收有一个基本原则就是对象是谁创建的,就要回收到创建线程对应的Stack中。所以回收线程就需要将池化对象回收至其创建线程对应的Stack中的WeakOrderQueue链表中。并等待创建线程将WeakOrderQueue链表中的待回收对象转移至Stack中的数组栈中。同样,回收线程也需要控制回收频率,由参数 DELAYED_QUEUE_RATIO 进行控制,默认也是8,可由JVM启动参数 -D io.netty.recycler.delayedQueue.ratio 指定,表示回收线程每处理完 8 个对象才回收 1 个对象。

8.Recycler对象池的创建

private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
    //recycler对象池实例
    private final Recycler<T> recycler;

    RecyclerObjectPool(final ObjectCreator<T> creator) {
         recycler = new Recycler<T>() {
            @Override
            protected T newObject(Handle<T> handle) {
                return creator.newObject(handle);
            }
        };
    }

    @Override
    public T get() {
        return recycler.get();
    }
}

Netty 中的 Recycler 对象池是一个抽象类,里面封装了对象池的核心结构以及核心方法。在创建对象池的时候,我们往往会使用Recycler的匿名类来实现抽象方法 newObject 从而来定义对象池创建对象的行为。

public abstract class Recycler<T> {
     // 对象池中线程对应的Stack可以存储池化对象默认最大个数 4096
    private static final int DEFAULT_MAX_CAPACITY_PER_THREAD;
  //    总共回收对象总量就是通过对象池的最大容量和该计算因子计算出来的。计算公式:max(maxCapacity  / maxSharedCapacityFactor, LINK_CAPACITY)
    //用于计算回收线程可帮助回收的最大容量因子  默认为2
    private static final int MAX_SHARED_CAPACITY_FACTOR;
      //创建线程回收对象时的回收比例,默认是8,表示只回收1/8的对象。也就是产生8个对象回收一个对象到对象池中
    private static final int RATIO;
      //每个回收线程最多可以帮助多少个创建线程回收对象 默认:cpu核数 * 2
    private static final int MAX_DELAYED_QUEUES_PER_THREAD;
      //回收线程回收对象时的回收比例,默认也是8,同样也是为了避免回收线程回收队列疯狂增长 回收比例也是1/8
    private static final int DELAYED_QUEUE_RATIO;
  
    protected Recycler() {
        this(DEFAULT_MAX_CAPACITY_PER_THREAD);
    }

    protected Recycler(int maxCapacityPerThread) {
        this(maxCapacityPerThread, MAX_SHARED_CAPACITY_FACTOR);
    }

    protected Recycler(int maxCapacityPerThread, int maxSharedCapacityFactor) {
        this(maxCapacityPerThread, maxSharedCapacityFactor, RATIO, MAX_DELAYED_QUEUES_PER_THREAD);
    }

    protected Recycler(int maxCapacityPerThread, int maxSharedCapacityFactor,
                       int ratio, int maxDelayedQueuesPerThread) {
        this(maxCapacityPerThread, maxSharedCapacityFactor, ratio, maxDelayedQueuesPerThread,
                DELAYED_QUEUE_RATIO);
    }

    //创建线程持有对象池的最大容量
    private final int maxCapacityPerThread;
    //所有回收线程可回收对象的总量(计算因子)
    private final int maxSharedCapacityFactor;
    //创建线程的回收比例
    private final int interval;
    //一个回收线程可帮助多少个创建线程回收对象
    private final int maxDelayedQueuesPerThread;
    //回收线程回收比例
    private final int delayedQueueInterval;
  
    protected Recycler(int maxCapacityPerThread, int maxSharedCapacityFactor,
                       int ratio, int maxDelayedQueuesPerThread, int delayedQueueRatio) {
        interval = max(0, ratio);
        delayedQueueInterval = max(0, delayedQueueRatio);
        if (maxCapacityPerThread <= 0) {
            this.maxCapacityPerThread = 0;
            this.maxSharedCapacityFactor = 1;
            this.maxDelayedQueuesPerThread = 0;
        } else {
            this.maxCapacityPerThread = maxCapacityPerThread;
            this.maxSharedCapacityFactor = max(1, maxSharedCapacityFactor);
            this.maxDelayedQueuesPerThread = max(0, maxDelayedQueuesPerThread);
        }
    }
  
    protected abstract T newObject(Handle<T> handle);

9. 多线程获取对象无锁化实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Netty对象池多线程获取对象时,为了避免多线程的开销,引入TLAB的思想,为每个线程分配了一个独立的stack结构,存储池化对象。当线程需要从对象池中获取对象时,Recycler会从线程对应的stack结构中获取池化对象。

//threadlocal保存每个线程对应的 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,
                interval, maxDelayedQueuesPerThread, delayedQueueInterval);
    }
...........
};

对象池中采用一个 FastThreadLocal 类型的字段 threadLocal 为每个线程维护一个独立的Stack结构。从而达到多线程无锁化获取对象的目的。当线程第一次从对象池中获取对象时会触发其对应的Stack结构的创建。

9.1 Stack结构的创建

private static final class Stack<T> {
    // 创建线程保存池化对象的stack结构所属对象池recycler实例
    final Recycler<T> parent;
    //用弱引用来关联当前stack对应的创建线程 因为用户可能在某个地方引用了defaultHandler -> stack -> thread,可能存在这个引用链
    //当创建线程死掉之后 可能因为这个引用链的存在而导致thread无法被回收掉
    final WeakReference<Thread> threadRef;
    //所有回收线程能够帮助当前创建线程回收对象的总容量
    final AtomicInteger availableSharedCapacity;
    //当前Stack对应的创建线程作为其他创建线程的回收线程时可以帮助多少个线程回收其池化对象
    private final int maxDelayedQueues;
    //当前创建线程对应的stack结构中的最大容量。 默认4096个对象
    private final int maxCapacity;
    //当前创建线程回收对象时的回收比例
    private final int interval;
    //当前创建线程作为其他线程的回收线程时回收其他线程的池化对象比例
    private final int delayedQueueInterval;
    // 当前Stack中的数组栈 默认初始容量256,最大容量为4096
    DefaultHandle<?>[] elements;
    //数组栈 栈顶指针
    int size;
    //回收对象计数 与 interval配合 实现只回收一定比例的池化对象
    private int handleRecycleCount;
    //多线程回收的设计,核心还是无锁化,避免多线程回收相互竞争
    //Stack结构中的WeakOrderQueue链表
    private WeakOrderQueue cursor, prev;
    private volatile WeakOrderQueue head;
  
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Recycler<T> parent:Stack所属Recycler对象池实例,一个对象池可被多个线程访问获取对象,所以一个对象池对应多个Stack,每个Stack的parent属性指向所属的Recycler实例。比如图中的 stack1 , stack2 , stack3 , stack4 中的parent属性均指向同一个Recycler对象池实例。
  • WeakReference<Thread> threadRef :Stack会通过弱引用的方式引用到其对应的创建线程。这里使用弱引用来持有对应创建线程的原因是因为对象池的设计中存在这样一个引用关系:池化对象 -> DefaultHandler -> stack -> threadRef。而池化对象是暴露给用户的,如果用户在某个地方持有了池化对象的强引用忘记清理,而Stack持有创建线程的强引用的话,当创建线程死掉的之后,因为这样一个强引用链的存在从而导致创建线程一直不能被GC回收。
  • AtomicInteger availableSharedCapacity:当前创建线程对应的所有回收线程可以帮助当前创建线程回收的对象总量。比如图中thread2 , thread3 , thread4 这三个回收线程总共可以帮助 thread1 回收对象的总量。availableSharedCapacity 在多个回收线程中是共享的,回收线程每回收一个对象它的值就会减1,当小于 LINK_CAPACITY(回收线程对应WeakOrderQueue节点的最小存储单元Link)时,回收线程将不能在为该stack回收对象了。该值的计算公式为前边介绍的 max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY)

当创建线程从Stack结构中的WeakOrderQueue链表中转移待回收对象到数组栈中后,availableSharedCapacity 的值也会相应增加。说白了这个值就是用来指示回收线程还能继续回收多少对象。已达到控制回收线程回收对象的总体容量。

  • int maxDelayedQueues:一个线程对于对象池来说,它可以是创建线程,也可以是回收线程,当该创建线程作为回收线程时,该值定义了最多可以为多少个创建线程回收对象。默认值为 CPU * 2。比如图中 thread2 作为回收线程既可以帮 thread1 回收对象也可以帮助 thread3 , thread4 回收对象。那么maxDelayedQueues 的值就是 3 。
  • int maxCapacity:定义当前Stack结构中的数组栈的最大容量。默认为4096。
  • int interval:创建线程的回收比例,默认是8。
  • int delayedQueueInterval:创建线程作为回收线程时的回收比例。默认是8。
  • DefaultHandle<?>[] elements:这个就是我们前边反复提到的Stack结构中的数组栈。用于存放对象池中的池化对象。当线程从对象池中获取对象时就是从这里获取。
  • int size:数组栈中的栈顶指针。
  • int handleRecycleCount:回收对象计数。与 interval 配合达到控制回收对象比例的目的。从 0 开始每遇到一个回收对象就 +1 ,同时把对象丢弃。直到handleRecycleCount == interval时回收对象,然后归零。也就是前边我们说到的每创建8个对象才回收1个。避免 Stack 不可控制的迅速增长。
  • WeakOrderQueue cursor, prev,head:这三个指针就是前边我们在讲Stack设计的时候介绍到的用于多线程无锁化回收的 WeakOrderQueue 链表中的头结点指针,当前节点指针,前一个节点指针(用于删除节点)。
private static final class Stack<T> {
    Stack(Recycler<T> parent, Thread thread, int maxCapacity, int maxSharedCapacityFactor,
          int interval, int maxDelayedQueues, int delayedQueueInterval) {
        this.parent = parent;
        threadRef = new WeakReference<Thread>(thread);
        this.maxCapacity = maxCapacity;
        availableSharedCapacity = new AtomicInteger(max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY));
        elements = new DefaultHandle[min(INITIAL_CAPACITY, maxCapacity)];
        this.interval = interval;
        this.delayedQueueInterval = delayedQueueInterval;
        handleRecycleCount = interval; // Start at interval so the first one will be recycled.
        this.maxDelayedQueues = maxDelayedQueues;
    }
}

9.2 从对象池中获取对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public abstract class Recycler<T> {
      //一个空的Handler,表示该对象不会被池化
    private static final Handle NOOP_HANDLE = new Handle() {
        @Override
        public void recycle(Object object) {
            // NOOP
        }
    };
  
    public final T get() {
        //如果对象池为0,则新建一个返回,并传入一个空的handle,表示该对象使用结束后不会进回收对象池
        if (maxCapacityPerThread == 0) {
            return newObject((Handle<T>) NOOP_HANDLE);
        }
        //获取当前线程保存池化对象的stack
        Stack<T> stack = threadLocal.get();
        //从栈pop出一个对象,handle是池化对象在对象池中的模型,包装了一些池化对象的回收信息和回收状态
        DefaultHandle<T> handle = stack.pop();
        //没池化对象则新建
        if (handle == null) {
            //初始化的handler对象recycleId和lastRecyclerId均为0
            handle = stack.newHandle();
            //newObject为对象池recycler的抽象方法,由使用者初始化内存池的时候 匿名提供
            handle.value = newObject(handle);
        }
        return (T) handle.value;
    } 

}
  1. 当对象池的最大容量maxCapacityPerThread == 0时,对象池会立马创建一个对象出来,并将一个空的Handler传递进对象中。表示该对象在使用完毕后不会被回收进对象池中。
  2. 从threadLocal中获取当前线程对应的Stack,随后从Stack结构中的数组栈中弹出栈顶对象的DefaultHandler。
  3. 如果弹出的DefaultHandler为空,说明当前Stack中并没有回收的池化对象。直接创建一个新的DefaultHandler并创建一个新的对象,然后将DefaultHandler传入到新创建的对象中,并用DefaultHandler包裹新创建的对象。这样池化对象就与DefaultHandler关联起来了。
static final class Entry {
    //recyclerHandle用于回收对象
    private final Handle<Entry> handle;
    //Entry的对象池,用来创建和回收Entry对象
    //静态变量引用类型地址 这个是在Klass Point(类型指针)中定义 8字节(开启指针压缩 为4字节)
    private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() {
        @Override
        public Entry newObject(Handle<Entry> handle) {
            return new Entry(handle);
        }
    });
  
          private Entry(Handle<Entry> handle) {
            this.handle = handle;
        }
}

9.3 DefaultHandler

public abstract class ObjectPool<T> {
    public interface Handle<T> {
        void recycle(T self);
    }
}

在Recycler对象池中的默认实现是 DefaultHandler ,DefaultHandler 里面包裹了池化对象以及池化对象在对象池中的一些相关信息,(比如:池化对象的相关回收信息和回收状态)。

从结构设计角度上来说,池化对象是隶属于其创建线程对应的Stack结构的,由于这层结构关系的存在,池化对象的DefaultHandler应该由Stack来进行创建。

private static final class Stack<T> {
        DefaultHandle<T> newHandle() {
            return new DefaultHandle<T>(this);
        }
}
private static final class DefaultHandle<T> implements Handle<T> {
    //标识最近被那个线程回收,被回收之前均是0
    volatile int lastRecycledId;
    //标识最终被那个线程回收,没被回收前是0
    int recycleId;
    //是否已经被回收
    boolean hasBeenRecycled;
    //强引用关联创建handler的stack
    Stack<?> stack;
    //池化对象
    Object value;

    DefaultHandle(Stack<?> stack) {
        this.stack = stack;
    }

    @Override
    public void recycle(Object object) {
    ..........
    }
 .......
}

DefaultHandler属性的第一部分信息,首先就是池化对象在对象池中的回收信息。

  • int lastRecycledId:用于标识最近被哪个线程回收,被回收之前均是0。
  • int recycleId:用于标识最终被哪个线程回收,在没被回收前是0。
  • boolean hasBeenRecycled:该池化对象是否已经被回收至创建线程对应的Stack中。

这里可能大家有疑问了,为什么池化对象的回收还要分最近回收和最终回收呢

因为对象池中的池化对象回收可以分为两种情况:

  • 由创建线程直接进行回收:这种回收情况就是一步到位,直接回收至创建线程对应的Stack中。所以这种情况下是不分阶段的。recycleId = lastRecycledId = OWN_THREAD_ID
  • 由回收线程帮助回收:这种回收情况下就要分步进行了,首先由回收线程将池化对象暂时存储在其创建线程对应Stack中的WeakOrderQueue链表中。此时并没有完成真正的对象回收。recycleId = 0,lastRecycledId = 回收线程Id(WeakOrderQueue#id)。当创建线程将WeakOrderQueue链表中的待回收对象转移至Stack结构中的数组栈之后,这时池化对象才算真正完成了回收动作。recycleId = lastRecycledId = 回收线程Id(WeakOrderQueue#id)

9.4 从Stack中获取池化对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里就是业务线程从对象池中真正获取池化对象的地方。从Stack结构中的数组栈的栈顶位置弹出池化对象。

  • 首先判断数组栈中是否有回收的池化对象。栈顶指针 size == 0 说明当前数组栈中是空的。随后就会调用 scavenge 方法,从Stack结构中的WeakOrderQueue链表中转移最多一个Link大小的待回收对象到数组栈中。如果WeakOrderQueue链表中也没有待回收对象,说明当前Stack结构就是空的没有任何回收的池化对象,对象池直接返回 null ,并创建一个新的池化对象返回给业务线程。
  • 如果数组栈不为空,则将栈顶元素 DefaultHandler 弹出,初始化池化对象DefaultHandler的回收信息。recycleId = lastRecycledId = 0表示该池化对象刚刚从对象池中取出。

recycleId 与 lastRecycledId 之间的关系分为以下几种情况:

  • recycleId = lastRecycledId = 0:表示池化对象刚刚被创建或者刚刚从对象池中取出即将被再次复用。这是池化对象的初始状态。
  • recycleId = lastRecycledId != 0:表示当前池化对象已经被回收至对应Stack结构里的数组栈中。可以直接被取出复用。可能是被其创建线程直接回收,也可能是被回收线程回收。
  • recycleId != lastRecycledId:表示当前池化对象处于半回收状态。池化对象已经被业务线程处理完毕,并被回收线程回收至对应的WeakOrderQueue节点中。并等待创建线程将其最终转移至Stack结构中的数组栈中。

9.4 转移回收线程回收的对象到Stack中

对象池中池化对象的回收存储分为两个部分:

  • 一个是池化对象直接被创建线程回收,直接存储在创建线程对应Stack结构中的数组栈中。
  • 另一个是池化对象被回收线程回收,临时间接存储在创建线程对应Stack结构中的WeakOrderQueue链表中。每个回收线程对应一个WeakOrderQueue节点。

当Stack结构中的数组栈为空时,创建线程会遍历WeakOrderQueue链表,从而将回收线程为其回收的对象从WeakOrderQueue节点中转移至数组栈中。多线程回收对象无锁化设计

这个转移的动作就是由 scavenge 方法来完成的。

private boolean scavenge() {
    // continue an existing scavenge, if any
    //从其他线程回收的weakOrderQueue里 转移 待回收对像 到当前线程的stack中
    if (scavengeSome()) {
        //有待回收对象则返回true
        return true;
    }

    // reset our scavenge cursor
    //重置cursor
    prev = null;
    cursor = head;
    return false;
}

scavengeSome() 执行具体的转移逻辑。如果WeakOrderQueue链表中还有待回收对象并转移成功则返回 true 。如果WeakOrderQueue链表为空没有任何待回收对象可转移,则重置链表相关的指针,cursor重新指向head节点,prev指向null。因为在遍历WeakOrderQueue链表搜寻可转移对象时,cursor指针已经发生变化了,这里需要重置。

9.5 转移回收对象

下面创建线程就开始遍历Stack结构中的WeakOrderQueue链表,将其中存储的回收线程回收进来的对象转移到数组栈中。

Stack中WeakOrderQueue链表的状态结构如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在Stack结构刚刚创建的初始状态,WeakOrderQueue链表是空的,所以 prev = head = cursor = null 。

后面当回收线程在回收对象时会加入自己对应的WeakOrderQueue节点到链表中。注意:WeakOrderQueue节点的插入都是在链表的头结点进行插入

head指针始终指向链表的头结点,cursor指针指向当前遍历的节点。在没有开始遍历链表前,cursor指针指向头结点。表示从头结点开始遍历。prev指针指向cursor前一个节点。当前遍历节点为头结点时,prev指针指向空。

链表的遍历转移过程逻辑:

private boolean scavengeSome() {
    WeakOrderQueue prev;
    WeakOrderQueue cursor = this.cursor;
    //在stack初始化完成后,cursor,prev,head等指针全部是null,这里如果cursor == null 意味着当前stack第一次开始扫描weakOrderQueue链表
    if (cursor == null) {
        prev = null;
        cursor = head;
        if (cursor == null) {
            //说明目前weakOrderQueue链表里还没有节点,并没有其他线程帮助回收的池化对象
            return false;
        }
    } else {
        //获取prev指针,用于操作链表(删除当前cursor节点)
        prev = this.prev;
    }

    boolean success = false;
    //循环遍历weakOrderQueue链表 转移待回收对象
    do {
        //将weakOrderQueue链表中当前节点中包含的待回收对象,转移到当前stack中,一次转移一个link
        if (cursor.transfer(this)) {
            success = true;
            break;
        }
        //如果当前cursor节点没有待回收对象可转移,那么就继续遍历链表获取下一个weakOrderQueue节点
        WeakOrderQueue next = cursor.getNext();
        //如果当前weakOrderQueue对应的回收线程已经挂掉了,则
        if (cursor.get() == null) {
            // If the thread associated with the queue is gone, unlink it, after
            // performing a volatile read to confirm there is no data left to collect.
            // We never unlink the first queue, as we don't want to synchronize on updating the head.
            // 判断当前weakOrderQueue节点是否还有可回收对象
            if (cursor.hasFinalData()) {
                //将节点所有待回收对象回收,因为线程已经dead了。该线程不会在帮助回收对象了。
                for (;;) {
                    if (cursor.transfer(this)) {
                        success = true;
                    } else {
                        break;
                    }
                }
            }
            //回收线程以死,对应的weaoOrderQueue节点中的最后一点待回收对象也已经回收完毕,就需要将当前节点从链表中删除。unlink当前cursor节点
            //这里需要注意的是,netty永远不会删除第一个节点,因为更新头结点是一个同步方法,避免更新头结点而导致的竞争开销
            // prev == null 说明当前cursor节点是头结点。不用unlink,如果不是头结点 就将其从链表中删除,因为这个节点不会再有线程来收集池化对象了
            if (prev != null) {
                // Ensure we reclaim all space before dropping the WeakOrderQueue to be GC'ed.
                //确保当前weakOrderQueue节点在被GC之前,我们已经回收掉它所有的占用空间
                //释放回收线程回收对象的availableSharedCapacity容量。释放的容量的大小为被删除WeakOrderQueue节点中存储的待回收对象容量。
                cursor.reclaimAllSpaceAndUnlink();
                //利用prev指针删除cursor节点
                prev.setNext(next);
            }
        } else {
            prev = cursor;
        }
        //向后移动prev,cursor指针继续遍历weakOrderQueue链表
        cursor = next;

    } while (cursor != null && !success);

    this.prev = prev;
    this.cursor = cursor;
    return success;
}
  1. 再开始遍历WeakOrderQueue链表之前,首先需要检查cursor指针是否为空,如果为空说明当前Stack是第一次开始遍历WeakOrderQueue链表。随后让cursor指针指向head指针,如果head指针指向为空,说明当前WeakOrderQueue链表是空的,此时没有任何回收线程在回收对象。如果head指针不为空,则从head指针指向的头结点开始遍历WeakOrderQueue链表。

  2. 首先会从cursor指针指向的当前遍历节点开始,将当前WeakOrderQueue节点中存储的待回收对象转移到Stack结构中的数组栈中。一次最多转移一个Link大小的对象。转移成功后退出。如果当前WeakOrderQueue节点此时没有任何待回收对象可被转移则转移失败,继续遍历下一个WeakOrderQueue节点。

    1. if (cursor.transfer(this)) {
          success = true;
          break;
      }
      
  3. 为了多线程能够无锁化回收对象,一个回收线程对应一个WeakOrderQueue节点,在WeakOrderQueue节点中持有对应回收线程的弱引用,目的也是为了当回收线程挂掉的时候,能够保证回收线程被GC及时的回收掉。如果cursor.get() == null说明当前WeakOrderQueue节点对应的回收线程已经挂掉了,此时如果当前节点还有待回收对象,则需要将节点中的所有待回收对象全部转移至Stack中的数组栈中。注意这里是转移节点所有的待回收对象而不是只转移一个Link。因为对应的回收线程已经挂掉了,该线程后续将不再会帮助创建线程回收对象了,所以要清理其对应的WeakOrderQueue节点。

private static final class WeakOrderQueue extends WeakReference<Thread> {
  ....
}
  1. 当清理完已经挂掉的回收线程对应的WeakOrderQueue节点后,就需要将该节点从Stack结构里的WeakOrderQueue链表中删除。保证被清理后的WeakOrderQueue节点可以被GC回收。当然删除节点之前需要通过cursor.reclaimAllSpaceAndUnlink()释放回收线程回收对象的availableSharedCapacity容量。释放的容量的大小为被删除WeakOrderQueue节点中存储的待回收对象容量。
if (prev != null) {
    // Ensure we reclaim all space before dropping the WeakOrderQueue to be GC'ed.
    //确保当前weakOrderQueue节点在被GC之前,我们已经回收掉它所有的占用空间
    //释放回收线程回收对象的availableSharedCapacity容量。释放的容量的大小为被删除WeakOrderQueue节点中存储的待回收对象容量。
    cursor.reclaimAllSpaceAndUnlink();
    //利用prev指针删除cursor节点
    prev.setNext(next);
}

Netty不会对WeakOrderQueue链表的头结点进行删除。如果prev == null说明当前节点是头结点,即使对应的回收线程已经挂掉了,但在本次遍历中不会对其进行删除。因为操作链表头结点的方法是一个同步方法,Netty这里是为了避免不必要的同步开销。

如果本次遍历的当前节点中并没有对象可转移,那么就继续从下一个节点开始遍历。循环执行转移逻辑直到遍历完链表或者中途转移成功。退出循环时要记录更新cursor指针记录当前遍历到的节点。

如果头结点对应的回收线程已经挂掉,这个头结点不在本次遍历中删除,那么会在什么时候被删除呢

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先当回收线程第一次开始帮助创建线程回收对象时,会将自己对应的WeakOrderQueue节点插入到创建线程对应Stack结构中的WeakOrderQueue链表的头结点位置。节点始终在链表的头结点位置插入

如图所示,当本次遍历发现头结点对应的回收线程 thread4 已经挂掉后,清理完头结点中存储的待回收对象后,让其继续呆在链表中,并不着急将其删除。随后cursor指针指向thread3对应的节点,下一次遍历就会从thread3对应的节点开始遍历。

当有一个新的回收线程 thread5 加入后,此时thread5对应的WeakOrderQueue节点变成了链表中的头结点,当经过多次遍历之后,cursor指针最终会再次指向死亡线程thread4对应的节点时,会再次进入cursor.get() == null的处理逻辑,而此时thread4对应的节点已经不是头结点了,所以在这次遍历中就将该节点从链表中删除。

操作WeakOrderQueue链表的头结点为什么是同步方法呢?

我们都知道一个回收线程对应一个WeakOrderQueue节点,当一个回收线程第一次为该创建线程回收对象时,都会新创建一个WeakOrderQueue节点并将节点插入到创建线程对应Stack中的WeakOrderQueue链表中的头结点位置。

在多线程回收场景下,可能会有多个回收线程同时向创建线程对应Stack中的WeakOrderQueue链表的头结点插入自己对应的节点。

那么此时对于链表头结点的操作就必须做同步处理了。当节点同步插入到链表的头结点后,以后该回收线程回收对象就是无锁化了。只不过就是在一开始插入节点的时候会有一点同步的开销,但是这是无法避免的

synchronized void setHead(WeakOrderQueue queue) {
    //始终在weakOrderQueue链表头结点插入新的queue(其他线程收集的由本线程创建的对象)
    queue.setNext(head);
    head = queue;
}

10. WeakOrderQueue的设计实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然该结构命名的后缀是一个Queue,但其实是一个链表,链表中的元素类型为Link,头结点指针Head永远指向第一个未被转移完毕的Link,当一个Link里的待回收对象被全部转移完毕后,head指针随即指向下一个节点,但是该Link节点并不会从链表中删除。尾指针Tail指向链表中最后一个Link节点。节点的插入是从链表的尾部开始插入。

10.1 Link结构

private static final class WeakOrderQueue extends WeakReference<Thread> {
    static final class Link extends AtomicInteger {
        //存储回收线程回收的对象
        final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];

        //创建线程在转移Link节点中的待回收对象时,通过这个readIndex来读取未被转移的对象。
        //由于readIndex只会被创建线程使用,所以这里并不需要保证原子性和可见性。用一个普通的int变量存储就好。
        int readIndex;
        Link next;
    }
}

首先我们从WeakOrderQueue的继承结构上来看,它继承于 WeakReference < Thread > , 表示该结构持有一个线程的弱引用,一个回收线程对应于一个WeakOrderQueue节点,很明显是持有其对应回收线程的弱引用,方便当回收线程挂掉的时候被GC回收。

  • DefaultHandle<?>[] elements : Link结构中包含一个容量为LINK_CAPACITY ,默认为16大小的DefaultHandle数组,用来存储回收线程回收的对象。
  • int readIndex:创建线程在转移Link节点中的待回收对象时,通过这个readIndex来读取未被转移的对象。由于readIndex只会被创建线程使用,所以这里并不需要保证原子性和可见性。用一个普通的int变量存储就好。
  • writeIndex:Link结构继承于AtomicInteger类型,这就意味着Link结构本身就可以被当做一个writeIndex来使用,由于回收线程在向Link节点添加回收对象的时候需要修改writeIndex,于此同时创建线程在转移Link节点的时候需要读取writeIndex,所以writeIndex需要保证线程安全性,故采用AtomicInteger类型存储。
  • Link next:Link节点的next指针,用于指向链表中的下一个节点。

10.2 Head结构

// weakOrderQueue内部link链表的头结点
private static final class Head {
    //所有回收线程能够帮助创建线程回收对象的总容量 reserveSpaceForLink方法中会多线程操作该字段
    //用于指示当前回收线程是否继续为创建线程回收对象,所有回收线程都可以看到,这个值是所有回收线程共享的。以便可以保证所有回收线程回收的对象总量不能超过availableSharedCapacity
    private final AtomicInteger availableSharedCapacity;

    Link link;

    Head(AtomicInteger availableSharedCapacity) {
        this.availableSharedCapacity = availableSharedCapacity;
    }

    //回收head节点的所有空间,并从链表中删除head节点,head指针指向下一节点
    void reclaimAllSpaceAndUnlink() {
       .......
    }

    //所有回收线程都可以看到,这个值是所有回收线程共享的。以便可以保证所有回收线程回收的对象总量不能超过availableSharedCapacity
    private void reclaimSpace(int space) {
        availableSharedCapacity.addAndGet(space);
    }

    //参数link为新的head节点,当前head指针指向的节点已经被回收完毕
    void relink(Link link) {
        reclaimSpace(LINK_CAPACITY);
        this.link = link;
    }

    Link newLink() {
        //此处的availableSharedCapacity可能已经被多个回收线程改变,因为availableSharedCapacity是用来控制回收线程回收的总容量限制
        //每个回收线程再回收对象时都需要更新availableSharedCapacity
        return reserveSpaceForLink(availableSharedCapacity) ? new Link() : null;
    }

    //为创建新的link预留空间容量
    static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) {
......
    }
}
  • AtomicInteger availableSharedCapacity:这个字段前边已经介绍过多次了,它是多线程共享的一个字段,可以被多个回收线程进行操作,表达的语义是所有回收线程总共可以帮助创建线程一共可以回收多少对象。对所有回收线程回收对象的总量进行限制。每创建一个Link节点,它的值就减少一个LINK_CAPACITY ,每释放一个Link节点,它的值就增加一个LINK_CAPACITY 。
  • Link link:Head结构封装的Link链表中的头结点。

10.3 WeakOrderQueue中的重要属性

private static final class WeakOrderQueue extends WeakReference<Thread> {
        //link链表的头结点,head指针始终指向第一个未被转移完毕的LinK节点
        private final Head head;
        //尾结点
        private Link tail;
        //站在stack的视角中,stack中包含一个weakOrderQueue的链表,每个回收线程为当前stack回收的对象存放在回收线程对应的weakOrderQueue中
        //这样通过stack中的这个weakOrderQueue链表,就可以找到其他线程为该创建线程回收的对象
        private WeakOrderQueue next;
        //回收线程回收Id,每个weakOrderQueue分配一个,同一个stack下的一个回收线程对应一个weakOrderQueue节点
        private final int id = ID_GENERATOR.getAndIncrement();
        //回收线程回收比例 默认是8
        private final int interval;
        //回收线程回收计数 回收1/8的对象
        private int handleRecycleCount;

  
}
  • Head head:用于指向WeakOrderQueue中Link链表的头结点。
  • Link tail:指向Link链表中的尾结点。
  • WeakOrderQueue next:站在Stack结构的视角上,Stack包含一个WeakOrderQueue链表,用来存放回收线程回收过来的池化对象。该字段为WeakOrderQueue节点的next指针,用于指向下一个回收线程对应的WeakOrderQueue节点。
  • int id :对应回收线程的回收Id,同一个Stack结构下,不同的回收线程对应不同的Id。
  • int interval:回收线程对应的回收频率,默认只回收 1 / 8 的池化对象。
  • int handleRecycleCount:回收对象计数,前边我们多次讲过了。用于控制回收频率。

10.4 WeakOrderQueue结构的创建

//为了使stack进行GC,这里不会持有其所属stack的引用
private WeakOrderQueue(Stack<?> stack, Thread thread) {
    //weakOrderQueue持有对应跨线程的弱引用
    super(thread);
    //创建尾结点
    tail = new Link();

    // 创建头结点  availableSharedCapacity = maxCapacity / maxSharedCapacityFactor
    // 此时availableSharedCapacity的值已经变化了,减去了一个link的大小
    head = new Head(stack.availableSharedCapacity);
    head.link = tail;
    interval = stack.delayedQueueInterval;
    handleRecycleCount = interval; 
}

在创建WeakOrderQueue结构的时候,首先会调用父类 WeakReference<Thread> 的构造方法持有当前回收线程的弱应用。

然后创建第一个Link节点,head指针和tail指针同时指向这第一个节点。

用创建线程对应的Stack中的属性初始化WeakOrderQueue结构中的相关属性。

大家这里可能会问了,既然这里用Stack中的属性去初始化WeakOrderQueue结构中的相关属性,那为什么WeakOrderQueue不直接持有Stack的引用呢

之前我们提到,一个回收线程对应一个WeakOrderQueue节点,当回收线程挂掉的时候,需要清理WeakOrderQueue节点并将其从Stack结构中的WeakOrderQueue链表(头结点除外)中删除。使得WeakOrderQueue节点可以被GC回收掉。

如果Stack结构对应的创建线程挂掉,而此时WeakOrderQueue又持有了Stack的引用,这样就使得Stack结构无法被GC掉。

所以这里只会用Stack结构的相关属性去初始化WeakOrderQueue结构,在WeakOrderQueue中并不会持有Stack的引用。

10.5 从WeakOrderQueue中转移回收对象

WeakOrderQueue的transfer方法用于将当前WeakOrderQueue节点中的待回收对象转移至创建线程对应的Stack中。

开始转移回收对象时会从WeakOrderQueue节点中的Link链表的头结点开始遍历,如果头结点中还有未被转移的对象,则将头结点剩余的未转移对象转移至Stack中。所以创建线程每次最多转移一个LINK_CAPACITY大小的对象至Stack中。只要成功转移了哪怕一个对象,transfer方法就会返回true。

如果头结点中存储的对象已经全部转移完毕,则更新head指针指向下一个Link节点,开始转移下一个Link节点。创建线程每次只会转移一个Link节点。如果Link链表是空的,没有转移成功一个对象,则transfer方法返回false。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

10.5.1 判断头结点中的待回收对象是否转移完毕
//获取当前weakOrderQueue节点中的link链表头结点
Link head = this.head.link;
//头结点为null说明还没有待回收对象
if (head == null) {
    return false;
}
//如果头结点中的待回收对象已经被转移完毕
if (head.readIndex == LINK_CAPACITY) {
    //判断是否有后续Link节点
    if (head.next == null) {
        //整个link链表没有待回收对象了已经
        return false;
    }
    head = head.next;
    //当前Head节点已经被转移完毕,head指针向后移动,head指针始终指向第一个未被转移完毕的LinK节点
    this.head.relink(head);
}
  1. 先查看head是否为空,为空则说明没待回收对象了,直接返回false。
  2. 不为空则说明head有回收对象,head.readIndex == LINK_CAPACITY 判断当前头结点对象是否转移完毕。如果转移完毕则更新为head的下一个节点,开始从下一个节点进行转移。下一个节点为空则返回false。
//参数link为新的head节点,当前head指针指向的节点已经被回收完毕
void relink(Link link) {
    reclaimSpace(LINK_CAPACITY);
    this.link = link;
}

//所有回收线程都可以看到,这个值是所有回收线程共享的。以便可以保证所有回收线程回收的对象总量不能超过availableSharedCapacity
private void reclaimSpace(int space) {
     availableSharedCapacity.addAndGet(space);
}
10.5.2 根据本次转移对象容量评估是否应该对Stack进行扩容

此时head节点校验完毕,接下来对本次转移对象的容量进行计算,评估stack是否容纳的下,容纳不下则需要扩容。

//获取已经转移的下标
final int srcStart = head.readIndex;
//尚未转移的下标
int srcEnd = head.get();
//获取该link节点可被转移的对象容量
final int srcSize = srcEnd - srcStart;
if (srcSize == 0) {
    return false;
}
// 获取创建线程stack中的当前回收对象数量总量
final int dstSize = dst.size;
// 待回收对象从weakOrderQueue中转移到stack后,stack的新容量 = 转移前stack容量 + 转移的待回收对象个数
final int expectedCapacity = dstSize + srcSize;

//如果转移后的stack容量超过当前stack的容量 则对stack进行扩容
if (expectedCapacity > dst.elements.length) {
    final int actualCapacity = dst.increaseCapacity(expectedCapacity);
    //每次转移最多一个Link的容量
    //actualCapacity - dstSize表示扩容后的stack还有多少剩余空间
    //转移对象最多不能超过stack的容量
    srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);
}

获取Link链表的readIndex和writeIndex,通过writeIndex-readIndex得出转移对象数量。

然后计算出stack转移对象之后的容量,expectedCapacity = stack当前容量 + 转移对象数量。

如果转移到stack对象容量 > stack最终容量,则进行扩容。然后根据扩容后的容量决定迁移多少数量对象,min(srcStart + actualCapacity - dstSize, srcEnd),确保不超过stack扩容后的容量。

private static final class Stack<T> {
          int increaseCapacity(int expectedCapacity) {
            int newCapacity = elements.length;
            int maxCapacity = this.maxCapacity;
            do {
                newCapacity <<= 1;
            } while (newCapacity < expectedCapacity && newCapacity < maxCapacity);
            //扩容后的新容量为最接近指定容量expectedCapacity的最大2的次幂
            newCapacity = min(newCapacity, maxCapacity);
            if (newCapacity != elements.length) {
                elements = Arrays.copyOf(elements, newCapacity);
            }

            return newCapacity;
        }
}

如果扩容之后的容量不满足转移对象数量,也就是则返回false。

if (srcStart != srcEnd) {
........
} else {
    // The destination stack is full already.
    return false;
}

如果Stack的容量可以容纳头结点中存储的待转移对象,则开始正式的转移逻辑

10.5.3 转移回收对象
    //待转移对象集合 也就是Link节点中存储的元素
    final DefaultHandle[] srcElems = head.elements;
    //stack中存储转移对象数组
    final DefaultHandle[] dstElems = dst.elements;
    int newDstSize = dstSize;
    for (int i = srcStart; i < srcEnd; i++) {
        DefaultHandle<?> element = srcElems[i];
        //recycleId == 0 表示对象还没有被真正的回收到stack中
        if (element.recycleId == 0) {
            //设置recycleId 表明是被哪个weakOrderQueue回收的
            //表示池化对象刚刚被创建或者刚刚从对象池中取出即将被再次复用。这是池化对象的初始状态。
            element.recycleId = element.lastRecycledId;
        } else if (element.recycleId != element.lastRecycledId) {
            //既被创建线程回收 同时也被回收线程回收  回收多次 则停止转移
            throw new IllegalStateException("recycled already");
        }
        //对象转移后需要置空Link节点对应的位置
        srcElems[i] = null;

        //这里从weakOrderQueue将待回收对象真正回收到所属stack之前 需要进行回收频率控制
        if (dst.dropHandle(element)) {
            // Drop the object.
            continue;
        }
        //重新为defaultHandler设置其所属stack(初始创建该handler的线程对应的stack)
        //该defaultHandler在被回收对象回收的时候,会将其stack置为null,防止极端情况下,创建线程挂掉,对应stack无法被GC

        /**
         * 这里在回收线程向WeakOrderQueue节点添加回收对象时先将 handle.stack设置为 null,而在转移回收对象时又将 handle.stack 设置回来,这不是多此一举吗?
         * 其实并不是多此一举,这样设计是非常有必要的,我们假设一种极端的情况,当创建线程挂掉并被GC回收之后,其实stack中存储的回收对象已经不可能在被使用到了,
         * stack应该也被回收掉。但是如果这里回收线程在回收的时候不将对象持有的stack设置为null的话,直接添加到了WeakOrderQueue节点中,
         * 当创建被GC掉的时候,由于这条引用链的存在导致对应stack永远不会被GC掉,造成内存泄露。
         */
        element.stack = dst;
        //此刻,handler才真正的被回收到所属stack中
        dstElems[newDstSize ++] = element;
    }

将当前Link节点中的elements数组里存储的对象转移至Stack中的数组栈elements中。转移范围 srcStart -> srcEnd

如果当前转移对象 element.recycleId == 0 说明当前对象还没有被真正的回收至创建线程对应的Stack中,符合转移条件(不能被多次回收)。还记不记得我们前边在《9.3 从Stack中获取池化对象》小节介绍的:

  • recycleId = lastRecycledId = 0:表示池化对象刚刚被创建或者刚刚从对象池中取出即将被再次复用。这是池化对象的初始状态。

随后设置回收Id element.recycleId = element.lastRecycledId。此处的lastRecycledId为当前WeakOrderQueue节点对应的回收线程Id。

element.recycleId != element.lastRecycledId 此处表示当前对象可能被创建线程回收了,也可能被回收线程回收了。

如果当前转移对象已经被回收至Stack中,则不能被再次回收,停止转移。

10.5.4 控制对象回收频率

符合转移条件的对象,需要再次经过回收频率的控制,即前边介绍的只回收 1 / 8 的对象,也就是每 8 个对象回收 1 个。

boolean dropHandle(DefaultHandle<?> handle) {
    if (!handle.hasBeenRecycled) {
        //回收计数handleRecycleCount 初始值为8 这样可以保证创建的第一个对象可以被池化回收
        //interval控制回收频率 8个对象回收一个
        if (handleRecycleCount < interval) {
            handleRecycleCount++;
            // Drop the object.
            return true;
        }
        //回收一个对象后,回收计数清零
        handleRecycleCount = 0;
        //设置defaultHandler的回收标识为true
        handle.hasBeenRecycled = true;
    }
    return false;
}

当对象通过了回收频率的验证之后,最后将回收对象的DefaultHandler中持有的Stack引用再次设置为其创建线程对应的Stack。因为在回收线程将池化对象回收至WeakOrderQueue节点时,会将其DefaultHandler中对Stack的引用置为null。所以这里需要重置回来。

随后会将对象压入Stack结构中的数组栈中,到这里,回收线程帮助创建线程回收的对象才算真正的被回收了,业务线程可以直接从对象池中取出使用了。

当对象转移完毕后,更新当前Link节点的readIndex,更新Stack中数组栈的栈顶指针。如果当前Link节点已经被转移完毕,则Head指针指向链表中的下一个节点,开始等待下一次的转移。

if (srcEnd == LINK_CAPACITY && head.next != null) {
    // Add capacity back as the Link is GCed.
    // 如果当前Link已经被回收完毕,且link链表还有后续节点,则更新head指针
    this.head.relink(head.next);
}

//更新已经转移的下标
head.readIndex = srcEnd;
//说明没有往stack添加转移对象,则返回false
if (dst.size == newDstSize) {
    return false;
}
//更新栈顶指针
dst.size = newDstSize;
return true;

11. 多线程回收对象无锁化实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

static final class Entry {
          //recyclerHandle用于回收对象
        private final Handle<Entry> handle;
          //将对象回收进对象池
        void recycle() {
            next = null;
            bufs = null;
            buf = null;
            msg = null;
            promise = null;
            progress = 0;
            total = 0;
            pendingSize = 0;
            count = -1;
            cancelled = false;
            handle.recycle(this);
        }
}
private static final class DefaultHandle<T> implements Handle<T> {
        //标识最近被那个线程回收,被回收之前均是0
        volatile int lastRecycledId;
        //标识最终被那个线程回收,没被回收前是0
        int recycleId;
        //强引用关联创建handler的stack
        Stack<?> stack;
        //池化对象
        Object value;
  
   @Override
        public void recycle(Object object) {
            if (object != value) {
                throw new IllegalArgumentException("object does not belong to handle");
            }

            //handler初次创建以及从对象池中获取到时  recycleId = lastRecycledId = 0(对象被回收之前)
            //创建线程回收对象后recycleId = lastRecycledId = OWN_THREAD_ID
            //回收线程回收对象后lastRecycledId = 回收线程Id,当对象被转移到stack中后 recycleId = lastRecycledId = 回收线程Id
            Stack<?> stack = this.stack;
            //lastRecycledId != recycleId:此时对象的状态正处于已经被回收线程回收至对应 WeakOrderQueue 节点的半回收状态,但还未被转移至其创建线程对应的Stack中。
                                        //所以这个条件要控制的事情就是如果对象已经被回收线程回收,那么就停止本次的回收操作。
            //stack == null 这种情况其实前边我们也有提到过,就是当池化对象对应的创建线程挂掉的时候,对应的Stack随后也被GC回收掉。那么这时就不需要在回收该池化对象了。
            if (lastRecycledId != recycleId || stack == null) {
                throw new IllegalStateException("recycled already");
            }

            stack.push(this);
        }
}
  • lastRecycledId != recycleId :此时对象的状态正处于已经被回收线程回收至对应 WeakOrderQueue 节点的半回收状态,但还未被转移至其创建线程对应的Stack中。所以这个条件要控制的事情就是如果对象已经被回收线程回收,那么就停止本次的回收操作
  • stack == null :当池化对象对应的创建线程挂掉的时候,对应的Stack随后也被GC回收掉。那么这时就不需要在回收该池化对象了。

11.1 回收对象至Stack中

void push(DefaultHandle<?> item) {
    Thread currentThread = Thread.currentThread();
    //判断当前线程是否为创建线程  对象池的回收原则是谁创建,最终由谁回收。其他线程只是将回收对象放入weakOrderQueue中
    //最终是要回收到创建线程对应的stack中的
    if (threadRef.get() == currentThread) {
        // The current Thread is the thread that belongs to the Stack, we can try to push the object now.
        // 如果当前线程正是创建对象的线程,则直接进行回收 直接放入与创建线程关联的stack中
        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.
        //当前线程不是创建线程,则将回收对象放入创建线程对应的stack中的weakOrderQueue链表相应节点中(currentThread对应的节点)
        pushLater(item, currentThread);
    }
}

11.2 创建线程直接回收对象

private void pushNow(DefaultHandle<?> item) {
    //池化对象被回收前 recycleId = lastRecycleId = 0
    //recycleId != 0 说明已经被回收了
    //lastRecycledId cas修改为OWN_THREAD_ID失败则说明已经有修改存在。
    if (item.recycleId != 0 || !item.compareAndSetLastRecycledId(0, OWN_THREAD_ID)) {
        throw new IllegalStateException("recycled already");
    }

    //此处是由创建线程回收,则将池化对象的recycleId与lastRecycleId设置为创建线程Id-OWN_THREAD_ID
    //注意这里的OWN_THREAD_ID是一个固定的值,是因为这里的视角是池化对象的视角,只需要区分创建线程和非创建线程即可。
    //对于一个池化对象来说创建线程只有一个 所以用一个固定的OWN_THREAD_ID来表示创建线程Id
    item.recycleId = OWN_THREAD_ID;

    int size = this.size;
    //如果当前池化对象的容量已经超过最大容量 则丢弃对象
    //为了避免池化对象的急速膨胀,这里只会回收1/8的对象,剩下的对象都需要丢弃
    if (size >= maxCapacity || dropHandle(item)) {
        // Hit the maximum capacity or should drop - drop the possibly youngest object.
        //丢弃对象
        return;
    }
    //当前线程对应的stack容量已满但是还没超过最大容量限制,则对stack进行扩容
    if (size == elements.length) {
        //容量扩大两倍
        elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
    }
    //将对象回收至当前stack中
    elements[size] = item;
    //更新当前stack的栈顶指针
    this.size = size + 1;
}
  1. 首先判断对象是否被回收了,item.recycleId和lastRecycledId任意一个为0则说明被回收了。
  2. 设置回收线程id为OWN_THREAD_ID
  3. 如果池化对象容量超过最大容量或者没有达到回收对象的频率则丢弃该对象
  4. stack容量已满且未超过最大容量则扩容
  5. 回收对象到stack中

11.5 回收线程间接回收对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在Recycler对象池中,一个线程既可以是创建线程也可以是回收线程。

比如上图中的 thread2 , thread3 , thread4 … 这里的每一个线程既可以在对象池中创建对象,并将对象回收至自己对应的Stack结构里的数组栈中,此刻它们的角色为创建线程。比如图中的thread1。

同时其他线程 比如图中的 thread2 , thread3 , thread4 … 也可以为thread1回收由thread1创建的对象,将这些对象回收至thread1对应的Stack结构里的WeakOrderQueue链表中。此刻 thread2 , thread3 , thread4 … 为回收线程。

在之前介绍Recycler对象池的重要属性时,我们提到过 maxDelayedQueuesPerThread 属性。

public abstract class Recycler<T> {
    //每个回收线程最多可以帮助多少个创建线程回收对象 默认:cpu核数 * 2
    private static final int MAX_DELAYED_QUEUES_PER_THREAD;
    //一个回收线程可帮助多少个创建线程回收对象
    private final int maxDelayedQueuesPerThread;
  
    private static final class Stack<T> {
        //当前Stack对应的创建线程作为其他创建线程的回收线程时可以帮助多少个线程回收其池化对象
        private final int maxDelayedQueues;
      }
}

在Recycler对象池中,一个回收线程能够帮助多少个创建线程回收对象是有限制的,通过 maxDelayedQueuesPerThread属性 控制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在对象池中,一个回收线程如何存储为其他创建线程回收到的对象呢

如图中所示,我们站在回收线程的视角来看,在对象池中有一个 FastThreadLocal 类型的 DELAYED_RECYCLED 字段, DELAYED_RECYCLED 为每个回收线程保存了一个 WeakHashMap,正是这个回收线程持有的 WeakHashMap 结构中保存了该回收线程为每个创建线程回收的对象。

WeakHashMap 结构中的 key 表示创建线程对应的 Stack 结构。意思是该回收线程为哪个创建线程回收对象。value 表示这个回收线程在创建线程中对应Stack结构里的WeakOrderQueue链表中对应的节点。大家在结合 《Recycler对象池.png》 这副图仔细体会下这个结构设计。

public abstract class Recycler<T> {    
	private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
            new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
        @Override
        protected Map<Stack<?>, WeakOrderQueue> initialValue() {
/*            这里为什么要用WeakHashMap呢?
            其实我们前边多少也提到过了,考虑到一种极端的情况就是当创建线程挂掉并且被GC回收之后,
            其实这个创建线程对应的Stack结构已经没有用了,存储在Stack结构中的池化对象永远不会再被使用到,
            此时回收线程完全就没有必要在为挂掉的创建线程回收对象了。而这个Stack结构如果没有任何引用链存在的话,
            随后也会被GC回收。那么这个Stack结构在WeakHashMap中对应的Entry也会被自动删除。
            如果这里不采用WeakHashMap,那么回收线程为该Stack回收的对象就会一直停留在回收线程中。*/
            return new WeakHashMap<Stack<?>, WeakOrderQueue>();
        }
    };
}

而这个WeakHashMap 的size即表示当前回收线程已经在为多少个创建线程回收对象了,size的值不能超过 maxDelayedQueuesPerThread 。

这里为什么要用WeakHashMap呢?

其实我们前边多少也提到过了,考虑到一种极端的情况就是当创建线程挂掉并且被GC回收之后,其实这个创建线程对应的Stack结构已经没有用了,存储在Stack结构中的池化对象永远不会再被使用到,此时回收线程完全就没有必要在为挂掉的创建线程回收对象了。而这个Stack结构如果没有任何引用链存在的话,随后也会被GC回收。那么这个Stack结构在WeakHashMap中对应的Entry也会被自动删除。如果这里不采用WeakHashMap,那么回收线程为该Stack回收的对象就会一直停留在回收线程中。

private void pushLater(DefaultHandle<?> item, Thread thread) {
    //maxDelayQueues == 0 表示不支持对象的跨线程回收
    if (maxDelayedQueues == 0) {
        // We don't support recycling across threads and should just drop the item on the floor.
        //直接丢弃
        return;
    }

    // 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();
    //获取当前回收对象属于的stack 由当前线程帮助其回收  注意这里是跨线程回收 当前线程并不是创建线程
    WeakOrderQueue queue = delayedRecycled.get(this);
    //queue == null 表示当前线程是第一次为该stack回收对象
    if (queue == null) {
        //maxDelayedQueues指示一个线程最多可以帮助多少个线程回收其创建的对象
        //delayedRecycled.size()表示当前线程已经帮助多少个线程回收对象
        if (delayedRecycled.size() >= maxDelayedQueues) {
            // Add a dummy queue so we know we should drop the object
            //如果超过指定帮助线程个数,则停止为其创建WeakOrderQueue,停止为其回收对象
            //WeakOrderQueue.DUMMY这里是一个标识,后边遇到这个标识  就不会为其回收对象了
            delayedRecycled.put(this, WeakOrderQueue.DUMMY);
            return;
        }
        // Check if we already reached the maximum number of delayed queues and if we can allocate at all.
        // 创建为回收线程对应的WeakOrderQueue节点以便保存当前线程为其回收的对象
        if ((queue = newWeakOrderQueue(thread)) == null) {
            // drop object
            // 创建失败则丢弃对象
            return;
        }
        //在当前线程的threadLocal中建立 回收对象对应的stack 与 weakOrderQueue的对应关系
        delayedRecycled.put(this, queue);
    } else if (queue == WeakOrderQueue.DUMMY) {
        // drop object
        // 如果queue的值是WeakOrderQueue.DUMMY 表示当前已经超过了允许帮助的线程数 直接丢弃对象
        return;
    }
    //当前线程为对象的创建线程回收对象  放入对应的weakOrderQueue中
    queue.add(item);
}
  1. 首先判断是否支持对象的跨线程回收 maxDelayedQueues == 0,不支持直接返回。
  2. 当前线程是第一次为该stack回收对象,且当前可帮助创建线程的个数超过了 maxDelayedQueues ,则向WeakHashMap塞入一个空的 WeakOrderQueue节点 DUMMY,后续如果遇到 WeakOrderQueue 节点是 DUMMY 实例则丢弃对象,放弃回收。
private static final class WeakOrderQueue extends WeakReference<Thread> {
	static final WeakOrderQueue DUMMY = new WeakOrderQueue();
}
  1. 当前线程是第一次为该stack回收对象,接下来创建WeakOrderQueue节点。创建之前判断是否超过了可帮助创建线程的个数 maxDelayedQueues。并在回收线程持有的WeakHashMap中建立Stack与回收线程对应的WeakOrderQueue节点的关联关系。
  2. 将回收对象添加至回收线程对应的WeakOrderQueue节点中,完成多线程无锁化回收。

11.6 为回收线程创建对应的WeakOrderQueue节点

private static final class Stack<T> {
      private WeakOrderQueue newWeakOrderQueue(Thread thread) {
          return WeakOrderQueue.newQueue(this, thread);
      }
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

private static final class WeakOrderQueue extends WeakReference<Thread> {
         static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
            // We allocated a Link so reserve the space
//            既然这里用Stack中的属性去初始化WeakOrderQueue结构中的相关属性,那为什么WeakOrderQueue不直接持有Stack的引用呢?
//            如果Stack结构对应的创建线程挂掉,而此时WeakOrderQueue又持有了Stack的引用,这样就使得Stack结构无法被GC掉。

            // link是weakOrderQueue中存储回收对象的最小结构,此处是为接下来要创建的Link预订空间容量
            // 如果stack指定的availableSharedCapacity 小于 LINK_CAPACITY大小,则分配失败
            if (!Head.reserveSpaceForLink(stack.availableSharedCapacity)) {
                return null;
            }

            //如果还够容量来分配一个link那么就创建weakOrderQueue
            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中的weakOrderQueue链表中添加当前回收线程对应的weakOrderQueue节点(始终在头结点处添加节点 )
            // 此处向stack中添加weakOrderQueue节点的操作被移到WeakOrderQueue构造器之外的目的是防止WeakOrderQueue.this指针
            // 逃逸避免被其他线程在其构造的过程中访问
            stack.setHead(queue);

            return queue;
        } 
}

在前边介绍WeakOrderQueue的结构的时候,我们提到WeakOrderQueue结构内部其实一个由Link节点组成的链表。WeakOrderQueue在初始状态下是只包含一个Link节点的链表。

所有在创建WeakOrderQueue结构的时候需要同时为其创建一个Link节点。而这些Link节点正是真正保存回收线程所回收到的对象的地方。

而对于一个创建线程来说它的所有回收线程能够为其回收对象的总量是被availableSharedCapacity 限制的,每创建一个Link节点,它的值就减少一个LINK_CAPACITY ,每释放一个Link节点,它的值就增加一个LINK_CAPACITY 。这样就能保证所有回收线程的回收总量不会超过 availableSharedCapacity 的限制。

所以在为WeakOrderQueue结构创建首个Link节点时,需要判断当前所有回收线程回收的对象总量是否已经超过了 availableSharedCapacity 。如果容量还够回收一个Link大小的对象,则开始创建WeakOrderQueue结构。

如果当前回收容量已经超过availableSharedCapacity或者不足回收一个Link大小的对象,则停止创建WeakOrderQueue节点,回收流程终止。不在对该回收对象进行回收。

//为创建新的link预留空间容量
static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) {
    for (;;) {
        //获取stack中允许异线程回收对象的总容量(异线程还能为该stack收集多少对象)
        int available = availableSharedCapacity.get();
        //当availbale可供回收容量小于一个Link时,说明异线程回收对象已经达到上限,不能在为stack回收对象了
        if (available < LINK_CAPACITY) {
            return false;
        }
        //为Link预留到一个Link的空间容量,更新availableSharedCapacity
        if (availableSharedCapacity.compareAndSet(available, available - LINK_CAPACITY)) {
            return true;
        }
    }
}

这里的预订容量其实就是将 availableSharedCapacity 的值减去一个 LINK_CAPACITY 大小。其他回收线程会看到这个 availableSharedCapacity 容量的变化,方便决定是否继续为创建线程回收对象。

当为WeakOrderQueue结构的首个Link节点预订容量成功后,就开始创建WeakOrderQueue节点。

//为了使stack进行GC,这里不会持有其所属stack的引用
private WeakOrderQueue(Stack<?> stack, Thread thread) {
    //weakOrderQueue持有对应跨线程的弱引用
    super(thread);
    //创建尾结点
    tail = new Link();

    // 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 = maxCapacity / maxSharedCapacityFactor
    // 此时availableSharedCapacity的值已经变化了,减去了一个link的大小
    head = new Head(stack.availableSharedCapacity);
    head.link = tail;
    interval = stack.delayedQueueInterval;
    handleRecycleCount = interval; // Start at interval so the first one will be recycled.
}

当回收线程对应的WeakOrderQueue节点创建成功后,就将其插入到回收对象对应的Stack结构里的WeakOrderQueue链表中的头结点处。因为这里可能会涉及多个回收线程并发向WeakOrderQueue链表头结点处添加节点,所以更新Stack结构中WeakOrderQueue链表头结点的方法被设计成同步方法。这也是整个Recycler 对象池设计中,唯一的一个同步方法。

synchronized void setHead(WeakOrderQueue queue) {
    //始终在weakOrderQueue链表头结点插入新的queue(其他线程收集的由本线程创建的对象)
    queue.setNext(head);
    head = queue;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11.7 向WeakOrderQueue节点中添加回收对象

将回收对象添加到回收线程对应的WeakOrderQueue节点中,Netty会在Link链表的尾结点处添加回收对象,如果尾结点容量已满,就继续新创建一个Link。将回收对象添加到新的Link节点中。

void add(DefaultHandle<?> handle) {
    //将handler中的lastRecycledId标记为当前weakOrderQueue中的Id,一个stack和一个回收线程对应一个weakOrderQueue节点
    //表示该池化对象 最近的一次是被当前回收线程回收的。
    if (!handle.compareAndSetLastRecycledId(0, id)) {
        // Separate threads could be racing to add the handle to each their own WeakOrderQueue.
        // We only add the handle to the queue if we win the race and observe that lastRecycledId is zero.
        return;
    }

    // While we also enforce the recycling ratio when we transfer objects from the WeakOrderQueue to the Stack
    // we better should enforce it as well early. Missing to do so may let the WeakOrderQueue grow very fast
    // without control
    // 控制异线程回收频率 只回收1/8的对象
    // 这里需要关注的细节是其实在scavengeSome方法中将weakOrderQueue中的待回收对象转移到创建线程的stack中时,Netty也会做回收频率的限制
    // 这里在回收线程回收的时候也会控制回收频率(总体控制两次)netty认为越早的做回收频率控制越好 这样可以避免weakOrderQueue中的容量迅速的增长从而失去控制
    if (handleRecycleCount < interval) {
        handleRecycleCount++;
        // Drop the item to prevent recycling to aggressive.
        return;
    }
    handleRecycleCount = 0;

    //从尾部link节点开始添加新的回收对象
    Link tail = this.tail;
    int writeIndex;
    //如果当前尾部link节点容量已满,就需要创建新的link节点
    if ((writeIndex = tail.get()) == LINK_CAPACITY) {
        //创建新的Link节点
        Link link = head.newLink();
        //如果availableSharedCapacity的容量不够了,则无法创建Link。丢弃待回收对象
        if (link == null) {
            // Drop it.
            return;
        }
        // We allocate a Link so reserve the space
        //更新尾结点
        this.tail = tail = tail.next = link;

        writeIndex = tail.get();
    }
    //将回收对象handler放入尾部link节点中
    tail.elements[writeIndex] = handle;
    //这里将stack置为null,是为了方便stack被回收。
    //如果Stack不再使用,期望被GC回收,发现handle中还持有stack的引用,那么就无法被GC回收,从而造成内存泄漏
    //在从对象池中再次取出该对象时,stack还会被重新赋予
    handle.stack = null;
    // we lazy set to ensure that setting stack to null appears before we unnull it in the owning thread;
    // this also means we guarantee visibility of an element in the queue if we see the index updated
    //注意这里用lazySet来延迟更新writeIndex。只有当writeIndex更新之后,在创建线程中才可以看到该待回收对象
    //保证线程最终可见而不保证立即可见的原因就是 其实这里Netty还是为了性能考虑避免执行内存屏障指令的开销。
    //况且这里也并不需要考虑线程的可见性,当创建线程调用scavengeSome从weakOrderQueue链表中回收对象时,看不到当前节点weakOrderQueue
    //新添加的对象也没关系,因为是多线程一起回收,所以继续找下一个节点就好。及时全没看到,大不了就在创建一个对象。主要还是为了提高weakOrderQueue的写入性能
    tail.lazySet(writeIndex + 1);
}
  1. 首先第一步就要设置回收对象DefaultHandler中的lastRecycledId ,将其设置为该回收线程Id,表示该回收对象最近一次是由当前回收线程回收的。此时的DefaultHandler中 recycleId != lastRecycledId ,对象处于半回收状态。
  2. 控制回收线程的回收频率(只回收 1 / 8 的对象),大家是否还记得我们在《9.5 转移回收对象》小节中介绍 stack#scavengeSome方法 的时候,在创建线程从Stack中的WeakOrderQueue链表中转移对象到数组栈中的时候,也会被回收频率进行控制,只转移 1 / 8 的对象。所以这里我们可以看到回收频率的控制在多线程回收对象的时候会控制两次,netty认为越早做回收频率控制越好这样可以避免weakOrderQueue中的容量迅速的增长从而失去控制。
  3. 在WeakOrderQueue结构中,当我们向Link链表添加回收对象时,都会向Link链表的尾结点中添加回收对象,如果当前尾结点容量已经满了 writeIndex = tail.get()) == LINK_CAPACITY ,我们就需要新创建一个Link节点,并将tail指针指向新的Link节点更新尾结点。最后将回收对象回收至新的尾结点中。当然我们要考虑到 availableSharedCapacity 容量的限制,如果容量不够了,就不能在新建Link节点,直接将回收对象丢弃,停止回收。
Link newLink() {
    //此处的availableSharedCapacity可能已经被多个回收线程改变,因为availableSharedCapacity是用来控制回收线程回收的总容量限制
    //每个回收线程再回收对象时都需要更新availableSharedCapacity
    return reserveSpaceForLink(availableSharedCapacity) ? new Link() : null;
}

//为创建新的link预留空间容量
static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) {
    for (;;) {
        //获取stack中允许异线程回收对象的总容量(异线程还能为该stack收集多少对象)
        int available = availableSharedCapacity.get();
        //当availbale可供回收容量小于一个Link时,说明异线程回收对象已经达到上限,不能在为stack回收对象了
        if (available < LINK_CAPACITY) {
            return false;
        }
        //为Link预留到一个Link的空间容量,更新availableSharedCapacity
        if (availableSharedCapacity.compareAndSet(available, available - LINK_CAPACITY)) {
            return true;
        }
    }
}

第一:为什么这里会将handle.stack设置为null?

不知大家还记不记得我们在介绍 stack#scavengeSome方法 的时候专门提到,在创建线程遍历WeakOrderQueue链表将链表中的待回收对象转移至stack中的数组栈时,会将待回收对象的DefaultHandler持有的stack重新设置为其创建线程对应的stack。

boolean transfer(Stack<?> dst) {

      .................省略..............

      //重新为defaultHandler设置其所属stack(初始创建该handler的线程对应的stack)
      //该defaultHandler在被回收对象回收的时候,会将其stack置为null,防止极端情况下,创建线程挂掉,对应stack无法被GC
      element.stack = dst;

      .................省略..............
}

而这里在回收线程向WeakOrderQueue节点添加回收对象时先将 handle.stack设置为 null,而在转移回收对象时又将 handle.stack 设置回来,这不是多此一举吗?

其实并不是多此一举,这样设计是非常有必要的,我们假设一种极端的情况,当创建线程挂掉并被GC回收之后,其实stack中存储的回收对象已经不可能在被使用到了,stack应该也被回收掉。但是如果这里回收线程在回收的时候不将对象持有的stack设置为null的话,直接添加到了WeakOrderQueue节点中,当创建被GC掉的时候,由于这条引用链的存在导致对应stack永远不会被GC掉,造成内存泄露。

第二:为什么最后使用lazySet来更新尾结点的writeIndex

当我们向Link链表的尾结点添加完回收对象之后,在更新尾结点的writeIndex时,使用到了延时更新,而延时更新并不会保证多线程的可见性,如果此时创建线程正在转移对象,那么将不会看到新添加进来的回收对象了。

而事实上,我们这里并不需要保证线程之间的实时可见性,只需要保证最终可见性即可。

确实在当创建线程转移对象的时候可能并不会看到刚刚被回收线程新添加进来的回收对象,看不到没关系,创建线程大不了在本次转移中不回收它不就完了么。因为只要创建线程Stack结构中的数组栈为空,创建线程就会从WeakOrderQueue链表中转移对象,以后会有很多次机会来WeakOrderQueu链表中转移对象,什么时候看见了,什么时候转移它。并不需要实时性。退一万步讲,即使全部看不到,大不了创建线程直接创建一个对象返回就行了。

而如果这里要保证线程之间的实时可见性,在更新尾结点的writeIndex的时候就不得不插入 LOCK 前缀内存屏障指令保证多线程之间的实时可见性,而执行内存屏障指令是需要开销的,所以为了保证WeakOrderQueue的写入性能,Netty这里选择了只保证最终可见性而不保证实时可见性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值