You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
source-code-hunter/docs/Netty/Netty技术细节源码分析/FastThreadLocal源码分析.md

153 lines
11 KiB

# 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的操作中得到其性能上带来的优势。
```
protected Thread newThread(Runnable r, String name) {
return new FastThreadLocalThread(threadGroup, r, name);
}
```
## FastThreadLocal的源码实现
### FastThreadLocal被访问的入口
当需要用到FastThreadLocal的时候想必和jdk原生的ThreadLocal的api类似都是通过初始化一个新的FastThreadLocal之后通过其set()方法初始化并放入一个变量作为线程本地变量存储。
```
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
set(InternalThreadLocalMap.get(), value);
} else {
remove();
}
}
```
因此在FastThreadLocal的set()方法中可以看到存储本地线程变量的数据结构是一个InternalThreadLocalMap。
```
private InternalThreadLocalMap threadLocalMap;
```
在FastThreadLocalThread 中因为本身threadLocalMap就是其中的一个成员能够快速得到返回。而其他线程实现就将面临没有这个成员的尴尬netty也给出了相应的兼容。
```
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。
```
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的内部构造
```
static final AtomicInteger nextIndex = new AtomicInteger();
Object[] indexedVariables;
```
InternalThreadlocalMap主要由以上两个成员组成其中indexedVariables作为一个Object[]数组直接用来存放FastThreadLocal对应的value每个FastThreadLocal对象都会在相应的线程的ThreadLocalMap中被分配到对应的index而这里的具体下标则由以上的nextIndex成员在每个FastThreadLocal初始化的时候分配。
```
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
```
每个FastThreadLocal在构造方法的过程中都会通过InternalThreadlocalMap的nextVariableIndex()返回nextIndex 自加后的结果作为其在InternalThreadlocalMap上的下标。后续该FastThreadLocal在操作变量的时候可以直接通过该index定位到Object[]数组上的位置。
```
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
```
而数组上的下标有一个特殊位一般在其首位也就是0的位置这个位置在FastThreadLocal类被加载的时候作为静态变量被设置。在这个位置上存放的是一个FastThreadLocal对象集合每个存放到InternalThreadlocalMap中的FastThreadLocal都会被保存在首位的集合中。
```
public static final Object UNSET = new Object();
```
另外为了具体区分保存的变量是null还是不存在当前变量InternalThreadLocalMap中定义了一个为NULL的成员变量以便区分上述情况在一开始InternalThreadLocalMap中的indexedVariables数组都是NULL。
### FastThreadLocal的set()方法的源码分析
相比FastThreadLocal的set操作get方法的过程与逻辑都要简单的多因此此处主要以其set方法为主。
```
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上的下标作为入参传入。
```
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的启发式检测开销。
```
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()方法。