Netty无锁化设计之对象池实现
池化技术是比较常见的一种技术,在平时我们已经就接触很多了,比如线程池,数据库连接池等等。当我们要使用一个资源的时候从池中去获取,用完就放回池中以便其他线程可以使用,这样的目的就是为了减少资源开销,提升性能。而Netty作为一个高性能的网络框架,在这一块也自然下足了工夫,下面我们就来看一下在Netty中的对象池是如何实现的吧
原理
与其他池化实现不同的是,其他的池化实现都是全局的,但是这样的话在实现的过程中就可能会有并发的问题,比如说在获取资源以及回收资源的时候都需要通过加锁等手段去处理,而Netty的对象池为了避免这些问题,采用了ThreadLocal去为每一个线程创造一个对象池,这样的话每一个线程去获取对象以及回收对象的时候就只会在自己所属的对象池中去操作了,自然就避免了加锁的过程处理,简单来说就是通过空间换时间的思想,从而达到了无锁化的目的。
不同的线程池使用独立的对象池虽然解决了上面加锁的问题,但是这也会导致另一个问题,比如说一个线程从自身的对象池中获取到了一个对象,但是这个对象被另外一个线程的对象池拿到并回收了,此时该对象就被回收到不属于自己的对象池中了。Netty为了解决这个问题,引入了一个队列,该队列就是专门存放这些帮助回收的线程回收的对象,举个例子,线程A从对象池中创建了一个对象,这个对象被线程B回收了,但是由于这个对象并不属于线程B的,所以线程B会为线程A创建一个队列,把这个回收对象放到这个队列中,当线程A再去从自身对象池中获取对象的时候,会先去这个队列中看是否有对象,如果有的话,就拿出来放回自身线程池中,这样就解决了上面的问题了。
所以可以看到,Netty对象池中很多都是通过线程隔离的思想去避免线程间并发竞争的情况出现,完全体现出了无锁化的设计思想。
源码解析
(1)整体设计
Recycler
public abstract class Recycler<T> {protected abstract T newObject(Handle<T> handle);
}
Recycler是整个对象池的一个外壳,其中提供了一个newObject的抽象方法,主要就是给子类去进行实现的,子类在可以在该方法中去创建出对象池中的对象
stack
private static final class Stack<T> {// 省略部分代码/*** 存放handle的数组,也可以认为是存放对象的数组*/DefaultHandle<?>[] elements;// 省略部分代码
}
private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {@Overrideprotected Stack<T> initialValue() {return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,interval, maxDelayedQueuesPerThread, delayedQueueInterval);}@Overrideprotected void onRemoval(Stack<T> value) {// Let us remove the WeakOrderQueue from the WeakHashMap directly if its safe to remove some overheadif (value.threadRef.get() == Thread.currentThread()) {if (DELAYED_RECYCLED.isSet()) {DELAYED_RECYCLED.get().remove(value);}}}
};
stack其实就是真正的对象池实现,当每一个线程想去从stack中获取对象的时候,都会去从上面的FastThreadLocal中去获取到该线程自己的stack,然后再从stack中获取对象。stack中有一个数组,该数组就是真是存放对象的,但是存放的数据类型是一个DefaultHandle,那我们的对象哪里呢?其实我们的对象就是在DefaultHandle里面,这里DefaultHandle帮我们的对象做了一层包装。
Handle
public interface Handle<T> extends ObjectPool.Handle<T> { }public abstract class ObjectPool<T> {public interface Handle<T> {void recycle(T self);}
}
private static final class DefaultHandle<T> implements Handle<T> {private static final AtomicIntegerFieldUpdater<DefaultHandle<?>> LAST_RECYCLED_ID_UPDATER;static {AtomicIntegerFieldUpdater<?> updater = AtomicIntegerFieldUpdater.newUpdater(DefaultHandle.class, "lastRecycledId");LAST_RECYCLED_ID_UPDATER = (AtomicIntegerFieldUpdater<DefaultHandle<?>>) updater;}/*** 当对象被其他帮助回收的线程回收了时候,该属性的值就是这个帮助回收的线程的id*/volatile int lastRecycledId;/*** 当原本创建该对象线程从其他帮助回收的线程中拿回该对象的时候,此时就会把lastRecycledId的值赋值给recycleId*/int recycleId;boolean hasBeenRecycled;Stack<?> stack;Object value;DefaultHandle(Stack<?> stack) {this.stack = stack;}@Overridepublic void recycle(Object object) {// 校验下回收的对象是否属于当前这个handleif (object != value) {throw new IllegalArgumentException("object does not belong to handle");}Stack<?> stack = this.stack;if (lastRecycledId != recycleId || stack == null) {throw new IllegalStateException("recycled already");}stack.push(this);}public boolean compareAndSetLastRecycledId(int expectLastRecycledId, int updateLastRecycledId) {// Use "weak…" because we do not need synchronize-with ordering, only atomicity.// Also, spurious failures are fine, since no code should rely on recycling for correctness.return LAST_RECYCLED_ID_UPDATER.weakCompareAndSet(this, expectLastRecycledId, updateLastRecycledId);}
}
DefaultHandle类实现了Recycler中的Handle接口,而Recycler中的Handle接口又继承于ObjectPool的handle接口,在ObjectPool的handle接口中有一个recycle方法,该方法就是用来回收对象的,传入的参数就是要回收的对象,当调用recycle方法的时候,最终会把当前的handle放到对应的stack中,由此可以知道回收对象的入口是在handle,而不是stack。另外一个重点的地方就是DefaultHandle的value属性,它其实就是我们说所需要的原始对象,总体来看,stack与handle的关系如下图所示:

WeakOrderQueue
当其他线程帮忙回收对象的时候会把对象存放在哪里呢?答案就是在WeakOrderQueue中,一个帮忙回收的线程针对每一个stack都会有一个WeakOrderQueue去存放回收这个stack的对象,并且WeakOrderQueue中通过link指针去构造出一个link链表,所以一个WeakOrderQueue也就代表着一个link链表。同理,既然WeakOrderQueue是用来存放回收对象的,那么这些回收对象也需要被取走是吧,所以在一个stack中,也构造了一个WeakOrderQueue链表,表示当前这个stack被哪些线程的WeakOrderQueue回收了对象,当需要取这些回收对象的时候,此时就可以取遍历这个WeakOrderQueue即可
Link
一个link对象中包含了一个DefaultHandle数组,这个数组存放的就是帮助其他stack回收的对象,每一个link之间则形成了链表
小总结
所以综上所述,stack,handle,WeakOrderQueue与link这4者之间的关系如下图所示(引用网上的一张图):

最后,如果我们要使用Netty的对象池会怎么去使用呢?在Netty中提供了模板ObjectPool类去对Recycler进行了包装,以便于我们能够更方便地使用,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> {private final Recycler<T> recycler;RecyclerObjectPool(final ObjectCreator<T> creator) {recycler = new Recycler<T>() {@Overrideprotected T newObject(Handle<T> handle) {return creator.newObject(handle);}};}@Overridepublic T get() {return recycler.get();}}
}
而ObjectPool可以如下使用:
ObjectPool<A> objectPool = ObjectPool.newPool(new ObjectPool.ObjectCreator<A>() {@Overridepublic A newObject(ObjectPool.Handle<A> handle) {return new A(handle);}
});A a = objectPool.get();
a.recycle();
当然了,我们创建的对象需要持有Handle,因为回收对象的方法是交给Handle去做的:
public class A {private final ObjectPool.Handle<A> handle;public A(ObjectPool.Handle<A> handle) {this.handle = handle;}public void recycle() {this.handle.recycle(this);}
}
(2)从stack中获取对象
DefaultHandle<T> pop() {// 获取到当前线程对应的stack中存在的对象的数量int size = this.size;// 条件成立:说明stack中没有对象if (size == 0) {// 此时需要去从WeakOrderQueue链表中获取对象,也就是从其他帮忙回收对象的线程的WeakOrderQueue中获取// 条件成立:表示其他帮忙回收对象的线程中也没有对象if (!scavenge()) {// 返回null,这样上层会创建新的对象return null;}size = this.size;if (size <= 0) {// double check, avoid racesreturn null;}}// 代码执行到这里说明此时stack中有存活的对象了,此时size数量-1size --;// 从数组中获取对象DefaultHandle ret = elements[size];elements[size] = null;// As we already set the element[size] to null we also need to store the updated size before we do// any validation. Otherwise we may see a null value when later try to pop again without a new element// added before.// 更新size数量this.size = size;if (ret.lastRecycledId != ret.recycleId) {throw new IllegalStateException("recycled multiple times");}// 重置这两个属性ret.recycleId = 0;ret.lastRecycledId = 0;// 返回对象return ret;
}
(3)其他线程帮忙回收对象
io.netty.util.Recycler.Stack#pushLater
如果代码来到这里,必定就是当前回收对象的线程与创建该对象的线程不是同一个线程
/*** 调用该方法的一定是帮助回收对象的线程。该方法会去把回收的对象放到对应的WeakOrderQueue中* @param item 回收的对象* @param thread 帮助回收对象的线程,也就是当前线程*/
private void pushLater(DefaultHandle<?> item, Thread thread) {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)// 获取这个帮助回收对象的线程对应的WeakOrderQueue,// key=>帮助哪个stack回收// value=>存放回收这个stack的对象的WeakOrderQueueMap<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();WeakOrderQueue queue = delayedRecycled.get(this);// 条件成立:说明当前线程之前还没有帮助过这个stack回收对象if (queue == null) {// 条件成立:说明已经超过最大帮助这个stack回收对象的线程上限了if (delayedRecycled.size() >= maxDelayedQueues) {// 把需要帮助回收对象的stack作为key放到map中,对应的value是一个dummy的WeakOrderQueuedelayedRecycled.put(this, WeakOrderQueue.DUMMY);return;}// 代码执行到这里说明此时还没有超过最大帮助这个stack回收对象的线程上限// 给当前stack创建WeakOrderQueue// 条件成立:说明当前stack已经没有回收对象的数量去分配了,已经不能再创建新的WeakOrderQueue了if ((queue = newWeakOrderQueue(thread)) == null) {// 放弃回收这个对象return;}// 把stack和对应的WeakOrderQueue放到线程map中delayedRecycled.put(this, queue);}// 在上面的if中当超过最大帮助这个stack回收对象的线程上限的时候,就会给map中放入一个dummy的WeakOrderQueueelse if (queue == WeakOrderQueue.DUMMY) {// 放弃回收这个对象return;}// 把要回收的对象放到对应的WeakOrderQueue中queue.add(item);
}
/*** 每一个线程都对应一个map* key=>帮助哪个stack回收对象* value=>帮助这个stack回收对象时,存放对象的WeakOrderQueue*/
private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {@Overrideprotected Map<Stack<?>, WeakOrderQueue> initialValue() {return new WeakHashMap<Stack<?>, WeakOrderQueue>();}
};
首先每个会去从FastThreadLocal中获取到一个map,map的key表示帮助的是哪一个stack去回收对象,value则是已经帮助这个stack回收的对象所存放的队列。
- 如果该线程是第一次帮助这个stack去回收对象,那么就先判断下该线程已经帮助过多少个stack回收对象,如果此时已经达到了帮助上限,那么此次就不能帮助这个stack回收了,然后就给这个stack创建一个DUMMY类型的WeakOrderQueue,以便下一次该线程再帮助这个stack回收对象的时候发现对应的队列是一个WeakOrderQueue能够立刻放弃回收
- 如果该线程不是第一次帮助这个stack回收对象了,那么能够获取到这个stack对应的WeakOrderQueue队列,反之则为这个stack创建一个新的WeakOrderQueue队列,并且把这个新创建的WeakOrderQueue队列放到map中,最后把要回收的对象放到WeakOrderQueue队列中
创建WeakOrderQueue队列
在上面回收的过程中,如果是第一次帮助这个stack回收,则需要为其创建一个新的WeakOrderQueue,代码如下:
private WeakOrderQueue newWeakOrderQueue(Thread thread) {
return WeakOrderQueue.newQueue(this, thread);
}
/*** 给指定的stack创建一个回收对象队列* 该方法可能会被并发调用,因为有可能此时会有多个帮助回收对象的线程在调用该方法去为指定的stack创建回收对象队列* @param stack 需要帮助回收对象的stack* @param thread 帮助回收对象的线程* @return WeakOrderQueue对象*/
static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {// 条件成立:说明指定的stack已经没有回收对象的数量去分配给当前线程去创建回收对象队列了if (!Head.reserveSpaceForLink(stack.availableSharedCapacity)) {// 返回nullreturn null;}// 为stack创建一个新的WeakOrderQueuefinal WeakOrderQueue queue = new WeakOrderQueue(stack, thread);// 把这个新创建的WeakOrderQueue放到stack的WeakOrderQueue链表的头部stack.setHead(queue);// 返回这个新创建的WeakOrderQueuereturn queue;
}
可以看到,创建WeakOrderQueue之前需要判断对这个stack的availableSharedCapacity属性进行判断,那么这个属性是什么意思呢?其实就是这个stack有多少个对象是能够被其他线程帮助回收的,比方说100个,那么其他帮助回收的线程回收这个stack的对象加起来一共最多是100个
static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) {for (;;) {// 获取到能够被其他线程回收的对象数量int available = availableSharedCapacity.get();// 条件成立:说明此时已经没有回收的对象数量了if (available < LINK_CAPACITY) {return false;}// 更新可回收的数量对象if (availableSharedCapacity.compareAndSet(available, available - LINK_CAPACITY)) {return true;}}
}
由于可能会有多个帮助这个stack回收对象的线程同时去对availableSharedCapacity进行扣减,所以availableSharedCapacity这里通过AtomicInteger修饰,保证并发的情况下扣减正确
当创建完WeakOrderQueue之后,把这个WeakOrderQueue放到这个stack的WeakOrderQueue链表的头部
/*** 把指定的WeakOrderQueue放到链表的头部,头插法。因为有可能会有多个线程同时去给当前的stack创建回收对象线程,所以该方法需要加锁执行,* 同时这也是整个Netty对象池中唯一加锁的地方* @param queue 指定的WeakOrderQueue*/
synchronized void setHead(WeakOrderQueue queue) {queue.setNext(head);head = queue;
}
因为上面我们也说了,这里可能会有多个线程调用,也就是说会有多个线程去创建WeakOrderQueue加到stack的链表头部,所以这里需要进行加锁操作,需要注意的是这也是整个Netty对象池中唯一一处加锁的地方,因为整个Netty对象池的设计就是为了无锁化去设计的
往WeakOrderQueue添加回收对象
/*** 把要回收的handle对象放到当前的WeakOrderQueue的尾link节点中* @param handle 要回收的handle对象*/
void add(DefaultHandle<?> handle) {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;}// 这里是为了控制对象回收的频率,默认每8次回收才能回收1个对象if (!handle.hasBeenRecycled) {if (handleRecycleCount < interval) {handleRecycleCount++;// Drop the item to prevent from recycling too aggressively.return;}handleRecycleCount = 0;}// 获取到link链表中的尾节点Link tail = this.tail;// 当前link的写指针int writeIndex;// 条件成立:说明这个link已经放满对象了if ((writeIndex = tail.get()) == LINK_CAPACITY) {// 创建一个新的link节点Link link = head.newLink();// 条件成立:说明这个stack中没有可帮助回收的对象数量了if (link == null) {// 放弃回收这个对象return;}// 把新创建的link节点设置为尾节点this.tail = tail = tail.next = link;// 重置写指针writeIndex = tail.get();}// 把回收对象放到link节点中tail.elements[writeIndex] = handle;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// 更新写指针tail.lazySet(writeIndex + 1);
}
WeakOrderQueue中是一个link节点的链表,每次添加回收对象的时候都是通过尾插法,获取到最后一个link节点的写指针,然后根据这个写指针把回收对象放入到这个link节点中。如果link节点已经放满了,那么就新创建出一个link节点(当然每次创建都要扣减availableSharedCapacity,如果扣减完了就返回null,此时就放弃回收这个对象了),并把这个link节点设置为尾节点,最后再把要回收的对象放入到这个新创建的link节点中
(4)从WeakOrderQueue中获取对象
io.netty.util.Recycler.Stack#scavengeSome
/*** 转移link链表中的对象,从头节点开始,如果这个link节点中的对象已经转移完了,就找下一个link节点进行转移,每调一次该方法就转移一个link节点* @return 当转移了一个link节点的时候返回true,反之返回false*/
private boolean scavengeSome() {// 前一个节点WeakOrderQueue prev;// 当前遍历到的节点WeakOrderQueue cursor = this.cursor;// 条件成立:说明此时是第一次遍历WeakOrderQueue链表if (cursor == null) {prev = null;cursor = head;// 条件成立:说明WeakOrderQueue为空,表示没有其他线程帮忙回收对象if (cursor == null) {return false;}}// 条件成立:不是第一次遍历WeakOrderQueue链表else {prev = this.prev;}boolean success = false;do {// 条件成立:说明转移了回收对象,此时跳出循环if (cursor.transfer(this)) {success = true;break;}// 代码执行到这里说明当前的WeakOrderQueue中没有转移到回收对象,此时获取下一个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中剩余的link节点中的对象进行转移if (cursor.hasFinalData()) {for (;;) {// 每遍历一次就把这个WeakOrderQueue中的一个link节点的回收对象转移一次if (cursor.transfer(this)) {success = true;} else {break;}}}if (prev != null) {// 释放这个WeakOrderQueue所占用的回收对象的数量cursor.reclaimAllSpaceAndUnlink();// 把当前这个WeakOrderQueue从链表中删除prev.setNext(next);}} else {prev = cursor;}// 把下一个节点当作当前节点,继续遍历cursor = next;} while (cursor != null && !success);// 重新赋值prev和cursorthis.prev = prev;this.cursor = cursor;return success;
}
当我们想要从stack中获取对象时,首先会从WeakOrderQueue链表中获取,从上面代码也可以看到,通过上面的代码可以看到,cursor指针表示的是当前遍历到的WeakOrderQueue,如果这个WeakOrderQueue中的对象已经获取完了,那么就继续从下一个节点去获取,并更新cursor指针,那么这里又是怎样从WeakOrderQueue中获取对象的呢?具体逻辑在transfer方法中:
/*** 把当前WeakOrderQueue对象中的link链表的头节点中的全部可回收对象转移到指定的stack中* @param dst 指定的stack* @return true=>有转移回收对象, false=>没有转移回收对象*/
@SuppressWarnings("rawtypes")
boolean transfer(Stack<?> dst) {Link head = this.head.link;// 条件成立:说明该WeakOrderQueue对象中还没有link链表if (head == null) {return false;}// 条件成立:说明当前头节点已经把回收对象全部转移完了if (head.readIndex == LINK_CAPACITY) {// 如果头节点的下一个节点为null,就说明没有回收对象可转移了if (head.next == null) {return false;}// 把下一个节点设置为头节点head = head.next;this.head.relink(head);}// 获取到当前的读指针final int srcStart = head.readIndex;// 获取到这个link节点中存放的回收对象数量int srcEnd = head.get();// 两者相减得到的就是需要转移的回收对象数量final int srcSize = srcEnd - srcStart;// 条件成立:说明没有可回收的对象能够转移if (srcSize == 0) {return false;}// 获取到转移目标的stack中的存活对象数量final int dstSize = dst.size;// 计算出当把可回收对象全部转移到stack中时,stack所需的大小final int expectedCapacity = dstSize + srcSize;// 条件成立:说明如果把可回收对象全部转移到stack中的时候,stack需要扩容if (expectedCapacity > dst.elements.length) {// 对stack进行扩容,得到扩容后的stack大小final int actualCapacity = dst.increaseCapacity(expectedCapacity);// actualCapacity - dstSize得到的就是stack扩容之后剩余的大小// 因为stack扩容之后有可能还是完全放不下link中可转移的回收对象,所以这里取最小值srcEnd = min(srcStart + actualCapacity - dstSize, srcEnd);}// 条件成立:说明link节点中存在可转移的回收对象if (srcStart != srcEnd) {// 获取到link节点中的对象数组final DefaultHandle[] srcElems = head.elements;// 获取到转移目标stack中的对象数组final DefaultHandle[] dstElems = dst.elements;int newDstSize = dstSize;// 遍历link节点的对象数组的可回收对象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)) {// 放弃回收该对象continue;}// 代码执行到这里说明该对象能够被回收// 重新给handle中的stack属性赋值element.stack = dst;// 把对象放到stack的对象数组中,此时就完成了该对象的回收dstElems[newDstSize ++] = element;}// 条件成立:说明这个link已经把可回收对象全部转移完了,此时把下一个link节点设置为头节点if (srcEnd == LINK_CAPACITY && head.next != null) {// Add capacity back as the Link is GCed.this.head.relink(head.next);}// 把可回收的对象转移完了之后更新读指针head.readIndex = srcEnd;// 条件成立:说明此次没有转移到一个回收对象if (dst.size == newDstSize) {return false;}// 代码执行到这里说明此次转移到至少一个回收对象,此时更新stack的sizedst.size = newDstSize;return true;} else {// The destination stack is full already.return false;}
}
我们从上面已经知道,每一个WeakOrderQueue对象其实就是一个由link节点组成的链表,而每一个link节点中存储对象靠的就是一个数组。当调用transfer方法的时候,首先会获取当前link链表的头节点,根据读指针和写指针计算出这个link节点转移对象的数组开始下标和结束下标,然后根据开始下标和结束下标把数组中的对象转移到stack中,最后判断这个link节点是否已经全部转移完里面的对象了,如果已经转移完了,就把下一个link节点置为头节点,这样当下一次再调用transfer方法的时候就能够又能从头节点获取了。也就是说每调用一次transfer方法,就会转移最多一个link节点数量的对象,如果一个对象都没有转移到,那么transfer方法就返回false,反之返回true。
分析了transfer方法,我们再回到scavengeSome方法中重新进行分析,在scavengeSome方法中大概的执行流程如下:
- 首先会通过cursor指针定位到已经遍历到的WeakOrderQueue对象,然后调用这个WeakOrderQueue对象的transfer方法进行对象转移
- 如果transfer方法返回true,则跳出do...while循环,反之如果返回false,则获取下一个WeakOrderQueue,并赋值给cursor指针,然后do...while循环重复上面第一点的步骤
- 如果transfer返回false,则说明这一次没有转移到对象,此时会再去判断当前这个WeakOrderQueue对应的线程是否已经挂了,如果挂了则再判断WeakOrderQueue中是否还存在没有被转移的link节点(因为有可能由于回收频率的控制导致这个link节点中本来有的对象被放弃回收了,但是该节点后可能还存在其他没有被转移的link节点),如果存在,则把剩余的这些link节点中的对象全部转移到stack中,最后再把这个WeakOrderQueue占用的回收对象数量归还给availableSharedCapacity属性,并且把这个WeakOrderQueue从链表中删除
总结来说scavengeSome方法会去遍历WeakOrderQueue链表,如果当前WeakOrderQueue转移不到对象,就换下一个WeakOrderQueue节点进行转移,当发现有WeakOrderQueue节点转移对象成功之后就返回true,反之返回false
相关文章:
Netty无锁化设计之对象池实现
池化技术是比较常见的一种技术,在平时我们已经就接触很多了,比如线程池,数据库连接池等等。当我们要使用一个资源的时候从池中去获取,用完就放回池中以便其他线程可以使用,这样的目的就是为了减少资源开销,…...
工厂生成中关于WiFi的一些问题
一 背景: 主要做高通和MTK,工厂生成中通过使用adb wifi,因为这样生产效率高并且避免了新机器有划痕,但是也经常碰到adb wifi无法连接的问题,那么是什么原因导致呢? 二 案例 测试步骤: 使用adb wifi连接手机测试工厂case adb usb adb tcpip 5555 adb connect DU…...
Java爬虫:获取商品评论数据的高效工具
在电子商务的激烈竞争中,商品评论作为消费者购买决策的重要参考,对于商家来说具有极高的价值。它不仅能够帮助商家了解消费者的需求和反馈,还能作为改进产品和服务的依据。Java爬虫技术,以其稳健性和高效性,成为了获取…...
oracle中的exists 和not exists 用法
exists (sql 返回结果集为真) not exists (sql 不返回结果集为真) exists 与 in 意思相同,语法不同,效率高于in not exists 与 not in 意思相同,语法不同,效率高于in 基本概念: se…...
自定义导出Excel数据注解实践
目录 前言结构组成定义自定义注解定义导出数据的实体定义Excel导出逻辑定义导出服务注解验证总结 前言 在企业级应用中,导入导出 Excel 文件是很常见的需求。通过使用自定义注解不仅可以实现灵活的 Excel 数据导入导出还可以减少手动配置的麻烦,提高代码…...
CSS3 动画相关属性实例大全(一)(@keyframes ,background属性,border 属性)
CSS3 动画相关属性实例大全(一) (keyframes ,background属性,border 属性) 本文目录: 零、时光宝盒 一、CSS3 动画基本概念 (1)、CSS3的动画基本属性 (2)…...
拦截器或过滤器往本次请求体中添加信息
步骤一:定义新的Request package com.ict.lux.framework.interceptor;import java.util.Collections; import java.util.Enumeration; import java.util.Map; import java.util.TreeMap;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.…...
Docker 安装达梦 DM8 数据库实战指南
Docker 安装达梦 DM8 数据库实战指南 文章目录 Docker 安装达梦 DM8 数据库实战指南一 安装环境二 下载 DM8 安装包三 导入镜像四 启动容器1)docker run 启动2)docker compose 启动3)名词解释 五 连接数据库 本文详细介绍了如何在 CentOS 7.9…...
QtCreator14调试Qt5.15出现 Launching Debugger 错误
1、问题描述 使用QtCreator14调试程序,Launching Debugger 显示红色,无法进入调试模式。 故障现象如下: 使能Debugger Log窗口,显示: 325^error,msg"Error while executing Python code." 不过ÿ…...
day1:基础了解
虚拟机网络设置 桥接模式:客户机使用宿主机的网段 使虚拟机像物理机一样直接连接到外部网络,拥有独立的IP地址,可与其他网络设备通信。 nat模式:客户机使用单独的局域网 通过宿主机的NAT功能,让虚拟机能够访问外部…...
【从零开始的LeetCode-算法】3099. 哈沙德数
如果一个整数能够被其各个数位上的数字之和整除,则称之为 哈沙德数(Harshad number)。给你一个整数 x 。如果 x 是 哈沙德数 ,则返回 x 各个数位上的数字之和,否则,返回 -1 。 示例 1: 输入&am…...
【Next.js 项目实战系列】02-创建 Issue
原文链接 CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 上一篇【Next.js 项目实战系列】01-创建项目 创建 Issue 配置 MySQL 与 Prisma 在数据库中可以找到相关内容&…...
浅谈C++的future
std::future 是 C 标准库中的一个模板类,提供了一种机制来管理和获取异步任务的结果。它常与异步操作相关,允许你在不同线程中执行任务,并在将来(即“未来”)某个时刻获取这些任务的结果。std::future 通常和 std::asy…...
期货外盘行情源7个市场CTP推送式服务说明
在期货交易领域,及时、准确的市场行情信息是投资者做出决策的重要依据。为了满足广大期货投资者对国际期货市场信息的迫切需求,我们特别推出了“期货外盘行情源2千每月7个市场CTP推送式”服务。本服务旨在通过高效、稳定的技术手段,为投资者提…...
计算机毕业设计 | SSM 校园线上订餐系统(附源码)
1, 概述 1.1 项目背景 传统的外卖方式就是打电话预定,然而,在这种方式中,顾客往往通过餐厅散发的传单来获取餐厅的相关信息,通过电话来传达自己的订单信息,餐厅方面通过电话接受订单后,一般通…...
【iOS】使用一个单例通过AFNetworking来实现网络请求
【iOS】使用一个单例通过AFNetworking来实现网络请求 文章目录 【iOS】使用一个单例通过AFNetworking来实现网络请求前言OC网络请求的流程 使用单例的原因创建一个单例采用AFNetworking的网络申请 小结 前言 笔者这周主要学习了第三方库AFNetworking的使用,这里笔者…...
如何从模块内部运行 Pytest
在 Python 中,pytest 是一个强大的测试框架,用于编写和运行测试用例。通常我们会在命令行中运行 pytest,但是有时你可能希望从模块或脚本的内部运行 pytest,比如为了自动化测试或集成到某个工作流程中。 1、问题背景 当你从模块…...
oracle数据库---基本查询(单表查询、多表查询、子查询、分页查询、oracle内置函数、行列转换、集合运算)
思维导图 单表查询 数据准备 -- 练习的表如果存在 请先删除 -- 如果不存在直接创建 drop table t_owners;--业主表 create table t_owners (id number primary key,name varchar2(30),addressid number,housenumber varchar2(30),watermeter varchar2(30),adddate date,owner…...
web API基础
作用和分类 作用: 就是使用 JS 去操作 html 和浏览器 分类: DOM (文档对象模型)、 BOM (浏览器对象模型) 什么是DOM DOM (Document Object Model) 译为文档对象模型,是 HTML 和 XML 文档的编程接口。 HTML DOM 定义了访问和操作 …...
【C++】创建TCP服务端
实现了一个基本的 TCP 服务器,可以接受多个客户端连接,然后持续接收客户端发送的信息, 最后将接收到的信息再发送回客户端 。 源码 头文件(TCPServerTest.h) #include <iostream> #include <winsock2.h&g…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
