最近在看netty源码的时候发现了一个叫FastThreadLocal的类,jdk自己自带了ThreadLocal类,因此能够大体想到此类比jdk自带的类速度更快,主要快在什么地方,以及为何速度更快,下面作一个简单的分析;数组
ThreadLocal主要被用在多线程环境下,方便的获取当前线程的数据,使用者无需关心多线程问题,方便使用;为了能说明问题,分别对两个场景进行测试,分别是:多个线程操做同一个ThreadLocal,单线程下的多个ThreadLocal,下面分别测试:多线程
分别对ThreadLocal和FastThreadLocal使用测试代码,部分代码以下:ide
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在性能上并无什么差距,下面对第二种状况进行测试;性能
分别对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更快;this
由于咱们经常使用的就是set和get方法,分别看一下对应的源码:spa
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和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。