Merge remote-tracking branch 'origin/master'

pull/63/head
AmyliaY 4 years ago
commit e0fc01445e

@ -158,6 +158,10 @@
* [Netty 架构设计](docs/Netty/AdvancedFeaturesOfNetty/Netty架构设计.md)
* [Netty 高性能之道](docs/Netty/AdvancedFeaturesOfNetty/Netty高性能之道.md)
### Netty 技术细节源码分析
* [FastThreadLocal源码分析](docs/Netty/Netty技术细节源码分析/FastThreadLocal源码分析.md)
* [Recycler对象池原理分析](docs/Netty/Netty技术细节源码分析/Recycler对象池原理分析.md)
## Dubbo
### 架构设计

@ -0,0 +1,186 @@
# Netty 的 FastThreadLocal 源码解析
该文中涉及到的 Netty 源码版本为 4.1.6。
## Netty 的 FastThreadLocal 是什么
> A special variant of ThreadLocal that yields higher access performance when accessed from a FastThreadLocalThread.
> Internally, a FastThreadLocal uses a constant index in an array, instead of using hash code and hash table, to look for a variable. Although seemingly very subtle, it yields slight performance advantage over using a hash table, and it is useful when accessed frequently.
> To take advantage of this thread-local variable, your thread must be a FastThreadLocalThread or its subtype. By default, all threads created by DefaultThreadFactory are FastThreadLocalThread due to this reason.
> Note that the fast path is only possible on threads that extend FastThreadLocalThread, because it requires a special field to store the necessary state. An access by any other kind of thread falls back to a regular ThreadLocal.
以上是 Netty 官方文档中关于 FastThreadLocal 的介绍。
简而言之FastThreadLocal 是在 ThreadLocal 实现上的一种变种,相比 ThreadLocal 内部通过将自身 hash 的方式在 hashTable 上定位需要的变量存储位置FastThreadLocal 选择在数组上的一个固定的常量位置来存放线程本地变量,这样的操作看起来并没有太大区别,但是相比 ThreadLocal 的确体现了性能上的优势,尤其是在读操作频繁的场景下。
## 如何使用 FastThreadLocal
如果想要得到 FastThreadLocal 的速度优势,必须通过 FastThreadLocalThread 或者其子类的线程才可以使用因为这个原因Netty 的 DefaultThreadFactory其内部默认线程工厂的 newThread()方法就是直接初始化一个 FastThreadLocalThread ,以便期望在 ThreadLocal 的操作中,得到其性能上带来的优势。
```java
protected Thread newThread(Runnable r, String name) {
return new FastThreadLocalThread(threadGroup, r, name);
}
```
## FastThreadLocal 的源码实现
### FastThreadLocal 被访问的入口
当需要用到 FastThreadLocal 的时候,想必和 jdk 原生的 ThreadLocal 的 api 类似,都是通过初始化一个新的 FastThreadLocal 之后,通过其 set()方法初始化并放入一个变量作为线程本地变量存储。
```java
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
set(InternalThreadLocalMap.get(), value);
} else {
remove();
}
}
```
因此,在 FastThreadLocal 的 set()方法中,可以看到,存储本地线程变量的数据结构是一个 InternalThreadLocalMap。
```java
private InternalThreadLocalMap threadLocalMap;
```
在 FastThreadLocalThread 中,因为本身 threadLocalMap 就是其中的一个成员能够快速得到返回。而其他线程实现就将面临没有这个成员的尴尬Netty 也给出了相应的兼容。
```java
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
```
InternalThreadLocalMap 的 get()方法中,当前线程如果是 FastThreadLocalThread 或是其子类的实现,变直接返回其 InternalThreadLocalMap 进行操作但对于不属于上述条件的线程Netty 通过 slowGet()的方式,也将返回一个 InternalThreadLocalMap。
```java
private static InternalThreadLocalMap slowGet() {
ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
```
在 slowGet()方法中,当前线程对应的 InternalThreadLocalMap 会通过原生 jdk 下 ThreadLocal 的方式存储并通过 ThreadLocal 返回,因此,在这个场景下,使用的还是 jdk 原生的 ThreadLocal但是只占用了原生 ThreadLocal 下的 Entry[]数组的一个位置,具体的变量还是存放在专门为 FastThreadLocal 服务的 InternalThreadLocalMap 中。
在此,随着 InternalThreadLocalMap 的得到并返回,针对 FastThreadLocal 的 get 和 set 操作,也将变为操作 InternalThreadLocalMap 来达到目的FastThreadLocal 性能优越的原因,也在 InternalThreadLocalMap 当中。
### InternalThreadLocalMap 的内部构造
```java
static final AtomicInteger nextIndex = new AtomicInteger();
Object[] indexedVariables;
```
InternalThreadlocalMap 主要由以上两个成员组成,其中 indexedVariables 作为一个 Object[]数组,直接用来存放 FastThreadLocal 对应的 value每个 FastThreadLocal 对象都会在相应的线程的 ThreadLocalMap 中被分配到对应的 index而这里的具体下标则由以上的 nextIndex 成员在每个 FastThreadLocal 初始化的时候分配。
```java
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
```
每个 FastThreadLocal 在构造方法的过程中,都会通过 InternalThreadlocalMap 的 nextVariableIndex()返回 nextIndex 自加后的结果作为其在 InternalThreadlocalMap 上的下标。后续该 FastThreadLocal 在操作变量的时候可以直接通过该 index 定位到 Object[]数组上的位置。
```java
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
```
而数组上的下标有一个特殊位,一般在其首位也就是 0 的位置,这个位置在 FastThreadLocal 类被加载的时候作为静态变量被设置。在这个位置上,存放的是一个 FastThreadLocal 对象集合,每个存放到 InternalThreadlocalMap 中的 FastThreadLocal 都会被保存在首位的集合中。
```java
public static final Object UNSET = new Object();
```
另外,为了具体区分保存的变量是 null 还是不存在当前变量InternalThreadLocalMap 中定义了一个为 NULL 的成员变量以便区分上述情况在一开始InternalThreadLocalMap 中的 indexedVariables 数组都是 NULL。
### FastThreadLocal 的 set()方法的源码分析
相比 FastThreadLocal 的 set 操作get 方法的过程与逻辑都要简单的多,因此此处主要以其 set 方法为主。
```java
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
set(InternalThreadLocalMap.get(), value);
} else {
remove();
}
}
public final void set(InternalThreadLocalMap threadLocalMap, V value) {
if (value != InternalThreadLocalMap.UNSET) {
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
} else {
remove(threadLocalMap);
}
}
```
在其 set()方法中,首先会判断 set 的值是否是 InternalThreadLocalMap 中的 NULL 对象来判断是 set 操作还是 remove 操作,如果不是,会通过 InternalThreadLocalMap.get()方法获取当前线程对应的 InternalThreadLocalMap获取的过程在前文已经描述过。
之后的主要流程主要分为两步:
- 调用 InternalThreadLocalMap 的 setIndexedVariable()方法,将该 FastThreadLocal 成员在构造方法中获得到的 InternalThreadLocalMap 上的下标作为入参传入。
```java
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
```
在 InternalThreadLocalMap 的 setIndexedVariable()方法过程中set 的过程并不复杂,找到对应的下标,并将对应的值放到 InternalThreadLocalMap 数组下标对应的位置上即宣告结束。但是,因为 FastThreadLocal 在构造过程中虽然预先获得了对应的下标,但是实际上的数组大小可能完全还没有达到相应的大小,就要在此处通过 expandIndexedVariableTableAndSet()方法进行扩容,由于是数组的缘故,只需要扩容后将原来的值复制过去,并将剩余的值用 NULL 对象填满即可。
- 如果上一步 set 成功,通过 addToVariablesToRemove()方法将该 FastThreadLocal 对象放入到 InternalThreadLocalMap 的数组中的首位集合中。在这个集合中,对于 FastThreadLocal 是一个强引用。
这样,对于 FastThreadLocal 的一次 set 操作即宣告结束。
## 相比 ThreadLocalFastThreadLocal 到底快在哪里
- FastThreadLocal 在具体的定位的过程中,只需要根据在构造方法里获取得到的具体下标就可以定位到具体的数组位置进行变量的存取,而在 jdk 原生的 ThreadLocal 中,具体位置的下标获取不仅需要计算 ThreadLocal 的 hash 值,并需要在 hashTable 上根据 key 定位的结果,一旦定位之后的结果上已经存在其他 ThreadLocal 的变量,那么则是通过线性探测法,在 hashTable 上寻找下一个位置进行,相比 FastThreadLocal 定位的过程要复杂的多。
- FastThreadLocal 由于采取数组的方式,当面对扩容的时候,只需要将原数组中的内容复制过去,并用 NULL 对象填满剩余位置即可,而在 ThreadLocal 中,由于 hashTable 的缘故,在扩容后还需要进行一轮 rehash在这过程中仍旧存在 hash 冲突的可能。
- 在 FastThreadLocal 中,遍历当前线程的所有本地变量,只需要将数组首位的集合即可,不需要遍历数组上的每一个位置。
- 在原生的 ThreadLocal 中,由于可能存在 ThreadLocal 被回收,但是当前线程仍旧存活的情况导致 ThreadLocal 对应的本地变量内存泄漏的问题,因此在 ThreadLocal 的每次操作后,都会进行启发式的内存泄漏检测,防止这样的问题产生,但也在每次操作后花费了额外的开销。而在 FastThreadLocal 的场景下,由于数组首位的 FastThreadLocal 集合中保持着所有 FastThreadLocal 对象的引用,因此当外部的 FastThreadLocal 的引用被置为 null该 FastThreadLocal 对象仍旧保持着这个集合的引用,不会被回收掉,只需要在线程当前业务操作后,手动调用 FastThreadLocal 的 removeAll()方法,将会遍历数组首位集合,回收掉所有 FastThreadLocal 的变量,避免内存泄漏的产生,也减少了原生 ThreadLocal 的启发式检测开销。
```java
private static final class DefaultRunnableDecorator implements Runnable {
private final Runnable r;
DefaultRunnableDecorator(Runnable r) {
this.r = r;
}
@Override
public void run() {
try {
r.run();
} finally {
FastThreadLocal.removeAll();
}
}
}
```
在 Netty 的 DefaultThreadFactory 中,每个线程在执行为任务后都会调用 FastThreadLocal 的 removeAll()方法。

@ -0,0 +1,143 @@
该文所涉及的 netty 源码版本为 4.1.6。
## Netty 的对象池 Recycler 是什么
Recycler 是 Netty 中基于 ThreadLocal 的轻量化的对象池实现。既然是基于 ThreadLocal那么就可以将其理解为当前线程在通过对象池 Recycler 得到一个对象之后,在回收对象的时候,不需要将其销毁,而是放回到该线程的对象池中即可,在该线程下一次用到该对象的时候,不需要重新申请空间创建,而是直接重新从对象池中获取。
## Recycler 在 netty 中被如何使用
Recycler 对象池在 netty 中最重要的使用,就在于 netty 的池化 ByteBuf 的场景下。首先,何为池化?以 PooledDirectByteBuf 举例,每一个 PooledDirectByteBuf 在应用线程中使用完毕之后,并不会被释放,而是等待被重新利用,类比线程池每个线程在执行完毕之后不会被立即释放,而是等待下一次执行的时候被重新利用。所谓的对象池也是如此,池化减少了 ByteBuf 创建和销毁的开销,也是 netty 高性能表现的基石之一。
```java
private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
@Override
protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
};
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
```
PooledDirectByteBuf 在其类加载的过程中,初始化了一个静态的 RECYCLER 成员,通过重写其 newObject()方法达到使 Recycler 可以初始化一个 PooledDirectByteBuf。而在接下来的使用中只需要通过静态方法 newInstance()就可以从 RECYCLER 对象池的 get()方法获取一个新的 PooledDirectByteBuf 对象返回,而重写的方法 newObject()中的入参 Handler 则提供了 recycle()方法给出了对象重新放入池中回收的能力这里的具体实现在下文展开。因此newInstance()方法和 recycle()方法就提供了对象池出池和入池的能力也通过此PooledDirectByteBuf 达到了池化的目标。
## Recycler 的实现原理分析
**Recycler 的实现原理很抽象,可以先直接阅读文末的例子再阅读这部分内容。**
Recycler 中,最核心的是两个通过 ThreadLocal 作为本地线程私有的两个成员,而其实现原理只需要围绕这两个成员分析,就可以对对象池的设计有直接的理解和认识。
- 第一个成员是在 Recycler 被定义的 Stack 成员对象。
```java
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,
ratioMask, maxDelayedQueuesPerThread);
}
};
```
顾名思义,这个 Stack 主体是一个堆栈,但是其还维护着一个链表,而链表中的每一个节点都是一个队列。
```java
private DefaultHandle<?>[] elements;
private WeakOrderQueue cursor, prev;
```
上述的 elements 数组便是存放当前线程被回收的对象,当当前线程从该线程的 Recycler 对象池尝试获取新的对象的时候,首先就会从当前 Stack 的这个数组中尝试获取已经在先前被创建并且在当前线程被回收的对象,因为当对象池的对象在当前线程被调用 recycle()的时候,是会直接放到 elements 数组中等待下一次的利用。 那么问题来了,如果从该线程中被申请的这个对象是在另外一个线程中被调用 recycle()方法回收呢?那么该对象就会处于链表中的队列中,当堆栈数组中的对象不存在的时候,将会尝试把链表队列中的对象转移到数组中供当前线程获取。那么其他线程是如何把被回收的对象放到这些链表中的队列的呢?接下来就是另一个成员的使命了。
- 第二个成员是在 Recycler 中也是通过 ThreadLocal 所实现的一个线程本地变量DELAYED_RECYCLED ,是一个 Stack 和队列的映射 Map。
```java
private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
@Override
protected Map<Stack<?>, WeakOrderQueue> initialValue() {
return new WeakHashMap<Stack<?>, WeakOrderQueue>();
}
};
```
第二个成员 DELAYED_RECYCLED 可以通过上文的 Stack 获取一个队列。
在前一个成员的解释中提到,当别的线程调用另一个线程的对象池的 recycle()方法进行回收的时候,并不会直接落到持有对象池的线程的 Stack 数组当中当然原因也很简单在并发情况下这样的操作显然是线程不安全的而加锁也会带来性能的开销。因此netty 在 Recycler 对象池中通过更巧妙的方式解决这一问题。
在前面提到除了数组Stack 还持有了一系列队列的组成的链表,这些链表中的每一个节点都是一个队列,这些队列又存放着别的线程所回收到当前线程对象池的对象。那么,这些队列就是各个线程针对持有对象池的专属回收队列,说起来很拗口,看下面的代码。
```java
private void pushLater(DefaultHandle<?> item, Thread thread) {
// 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();
WeakOrderQueue queue = delayedRecycled.get(this);
if (queue == null) {
if (delayedRecycled.size() >= maxDelayedQueues) {
// Add a dummy queue so we know we should drop the object
delayedRecycled.put(this, WeakOrderQueue.DUMMY);
return;
}
// Check if we already reached the maximum number of delayed queues and if we can allocate at all.
if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
// drop object
return;
}
delayedRecycled.put(this, queue);
} else if (queue == WeakOrderQueue.DUMMY) {
// drop object
return;
}
queue.add(item);
}
private WeakOrderQueue(Stack<?> stack, Thread thread) {
head = tail = new Link();
owner = new WeakReference<Thread>(thread);
synchronized (stack) {
next = stack.head;
stack.head = this;
}
// 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 = stack.availableSharedCapacity;
}
```
pushLater()方法发生在当一个对象被回收的时候,当当前线程不是这个对象所申请的时候的线程时,将会通过该对象的 Stack 直接去通过 DELAYED_RECYCLED 映射到一条队列上,如果没有则创建并建立映射,再把该对象放入到该队列中,以上操作结束后该次回收即宣告结束
```java
private WeakOrderQueue(Stack<?> stack, Thread thread) {
head = tail = new Link();
owner = new WeakReference<Thread>(thread);
synchronized (stack) {
next = stack.head;
stack.head = this;
}
// 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 = stack.availableSharedCapacity;
}
```
如果在操作中,队列是被创建的,会把该队列放置在 Stack 中的链表里的头结点,保证创建该对象的线程在数组空了之后能够通过链表访问到该队列并将该队列中的回收对象重新放到数组中等待被下次重新利用,队列交给 A 线程的链表是唯一的阻塞操作。在这里通过一次阻塞操作,避免后续都不存在资源的竞争问题。
## 举一个例子来解释对象池的原理
_A 线程申请A 线程回收的场景。_
- 显然,当对象的申请与回收是在一个线程中时,直接把对象放入到 A 线程的对象池中即可,不存在资源的竞争,简单轻松。
_A 线程申请B 线程回收的场景。_
- 首先,当 A 线程通过其对象池申请了一个对象后,在 B 线程调用 recycle()方法回收该对象。显然,该对象是应该回收到 A 线程私有的对象池当中的,不然,该对象池也失去了其意义。
- 那么 B 线程中,并不会直接将该对象放入到 A 线程的对象池中,如果这样操作在多线程场景下存在资源的竞争,只有增加性能的开销,才能保证并发情况下的线程安全,显然不是 netty 想要看到的。
- 那么 B 线程会专门申请一个针对 A 线程回收的专属队列,在首次创建的时候会将该队列放入到 A 线程对象池的链表首节点(这里是唯一存在的资源竞争场景,需要加锁),并将被回收的对象放入到该专属队列中,宣告回收结束。
- 在 A 线程的对象池数组耗尽之后,将会尝试把各个别的线程针对 A 线程的专属队列里的对象重新放入到对象池数组中,以便下次继续使用。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Loading…
Cancel
Save