前言
最近在看 netty 源码的时候发现了一个叫 FastThreadLocal 的类,jdk 本身自带了 ThreadLocal 类,所以可以大致想到此类比 jdk 自带的类速度更快,主要快在什么地方,以及为什么速度更快,下面做一个简单的分析;
性能测试
ThreadLocal 主要被用在多线程环境下,方便的获取当前线程的数据,使用者无需关心多线程问题,方便使用;为了能说明问题,分别对两个场景进行测试,分别是:多个线程操作同一个 ThreadLocal,单线程下的多个 ThreadLocal,下面分别测试:
1. 多个线程操作同一个 ThreadLocal
分别对 ThreadLocal 和 FastThreadLocal 使用测试代码,部分代码如下:
public static void test2() throws Exception {CountDownLatch cdl = new CountDownLatch(10000);
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
long starTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {new Thread(new Runnable() {
@Override
public void run() {threadLocal.set(Thread.currentThread().getName());
for (int k = 0; k < 100000; k++) {threadLocal.get();
}
cdl.countDown();}
}, "Thread" + (i + 1)).start();}
cdl.await();
System.out.println(System.currentTimeMillis() - starTime + "ms");
}
以上代码创建了 10000 个线程,同时往 ThreadLocal 设置,然后 get 十万次,然后通过 CountDownLatch 来计算总的时间消耗,运行结果为:1000ms 左右 ;
下面再对 FastThreadLocal 进行测试,代码类似:
public static void test2() throws Exception {CountDownLatch cdl = new CountDownLatch(10000);
FastThreadLocal<String> threadLocal = new FastThreadLocal<String>();
long starTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {new FastThreadLocalThread(new Runnable() {
@Override
public void run() {threadLocal.set(Thread.currentThread().getName());
for (int k = 0; k < 100000; k++) {threadLocal.get();
}
cdl.countDown();}
}, "Thread" + (i + 1)).start();}
cdl.await();
System.out.println(System.currentTimeMillis() - starTime);
}
运行之后结果为:1000ms 左右;可以发现在这种情况下两种类型的 ThreadLocal 在性能上并没有什么差距,下面对第二种情况进行测试;
2. 单线程下的多个 ThreadLocal
分别对 ThreadLocal 和 FastThreadLocal 使用测试代码,部分代码如下:
public static void test1() throws InterruptedException {
int size = 10000;
ThreadLocal<String> tls[] = new ThreadLocal[size];
for (int i = 0; i < size; i++) {tls[i] = new ThreadLocal<String>();}
new Thread(new Runnable() {
@Override
public void run() {long starTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {tls[i].set("value" + i);
}
for (int i = 0; i < size; i++) {for (int k = 0; k < 100000; k++) {tls[i].get();}
}
System.out.println(System.currentTimeMillis() - starTime + "ms");
}
}).start();}
以上代码创建了 10000 个 ThreadLocal,然后使用同一个线程对 ThreadLocal 设值,同时 get 十万次,运行结果:2000ms 左右 ;
下面再对 FastThreadLocal 进行测试,代码类似:
public static void test1() {
int size = 10000;
FastThreadLocal<String> tls[] = new FastThreadLocal[size];
for (int i = 0; i < size; i++) {tls[i] = new FastThreadLocal<String>();}
new FastThreadLocalThread(new Runnable() {
@Override
public void run() {long starTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {tls[i].set("value" + i);
}
for (int i = 0; i < size; i++) {for (int k = 0; k < 100000; k++) {tls[i].get();}
}
System.out.println(System.currentTimeMillis() - starTime + "ms");
}
}).start();}
运行结果:30ms 左右;可以发现性能达到两个数量级的差距,当然这是在大量访问次数的情况下才有的效果;下面重点分析一下 ThreadLocal 的机制,以及 FastThreadLocal 为什么比 ThreadLocal 更快;
ThreadLocal 的机制
因为我们常用的就是 set 和 get 方法,分别看一下对应的源码:
public void set(T value) {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
以上代码大致意思:首先获取当前线程,然后获取当前线程中存储的 threadLocals 变量,此变量其实就是 ThreadLocalMap,最后看此 ThreadLocalMap 是否为空,为空就创建一个新的 Map,不为空则以当前的 ThreadLocal 为 key,存储当前 value;可以进一步看一下 ThreadLocalMap 中的 set 方法:
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();}
大致意思:ThreadLocalMap 内部使用一个数组来保存数据,类似 HashMap;每个 ThreadLocal 在初始化的时候会分配一个 threadLocalHashCode,然后和数组的长度进行取模操作,所以就会出现 hash 冲突的情况,在 HashMap 中处理冲突是使用数组 + 链表的方式,而在 ThreadLocalMap 中,可以看到直接使用 nextIndex,进行遍历操作,明显性能更差;下面再看一下 get 方法:
public T get() {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();}
同样是先获取当前线程,然后获取当前线程中的 ThreadLocalMap,然后以当前的 ThreadLocal 为 key,到 ThreadLocalMap 中获取 value:
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
int len = tab.length;
while (e != null) {ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
同 set 方式,通过取模获取数组下标,如果没有冲突直接返回数据,否则同样出现遍历的情况;所以通过分析可以大致知道以下几个问题:
1.ThreadLocalMap 是存放在 Thread 下面的,ThreadLocal 作为 key,所以多个线程操作同一个 ThreadLocal 其实就是在每个线程的 ThreadLocalMap 中插入的一条记录,不存在任何冲突问题;
2.ThreadLocalMap 在解决冲突时,通过遍历的方式,非常影响性能;
3.FastThreadLocal 通过其他方式解决冲突的问题,达到性能的优化;
下面继续来看一下 FastThreadLocal 是通过何种方式达到性能的优化。
为什么 Netty 的 FastThreadLocal 速度快
Netty 中分别提供了 FastThreadLocal 和 FastThreadLocalThread 两个类,FastThreadLocalThread 继承于 Thread,下面同样对常用的 set 和 get 方法来进行源码分析:
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);
}
}
此处首先对 value 进行判定是否为 InternalThreadLocalMap.UNSET,然后同样使用了一个 InternalThreadLocalMap 用来存放数据:
public static InternalThreadLocalMap get() {Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {return fastGet((FastThreadLocalThread) thread);
} else {return slowGet();
}
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
可以发现 InternalThreadLocalMap 同样存放在 FastThreadLocalThread 中,不同在于,不是使用 ThreadLocal 对应的 hash 值取模获取位置,而是直接使用 FastThreadLocal 的 index 属性,index 在实例化时被初始化:
private final int index;
public FastThreadLocal() {index = InternalThreadLocalMap.nextVariableIndex();
}
再进入 nextVariableIndex 方法中:
static final AtomicInteger nextIndex = new AtomicInteger();
public static int nextVariableIndex() {int index = nextIndex.getAndIncrement();
if (index < 0) {nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
在 InternalThreadLocalMap 中存在一个静态的 nextIndex 对象,用来生成数组下标,因为是静态的,所以每个 FastThreadLocal 生成的 index 是连续的,再看一下 InternalThreadLocalMap 中是如何 setIndexedVariable 的:
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;
}
}
indexedVariables 是一个对象数组,用来存放 value;直接使用 index 作为数组下标进行存放;如果 index 大于数组长度,进行扩容;get 方法直接通过 FastThreadLocal 中的 index 进行快速读取:
public final V get(InternalThreadLocalMap threadLocalMap) {Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {return (V) v;
}
return initialize(threadLocalMap);
}
public Object indexedVariable(int index) {Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
直接通过下标进行读取,速度非常快;但是这样会有一个问题,可能会造成空间的浪费;
总结
通过以上分析我们可以知道在有大量的 ThreadLocal 进行读写操作的时候,才可能会遇到性能问题;另外 FastThreadLocal 通过空间换取时间的方式来达到 O(1)读取数据;还有一个疑问就是内部为什么不直接使用 HashMap(数组 + 黑红树)来代替 ThreadLocalMap。