java.lang.ThreadLocal
ThreadLocal类通过线程封闭的方式解决线程安全,提到它,大家都会想到弱引用和内存泄漏等话题,它的get/set/remove等方法,网上有很多关于它的话题和详细介绍.这篇文章不会介绍这些内容.
ThreadLocal作为key被存放到线程的ThreadLocal.ThreadLocalMap中.我们简单看下它的set方法.
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// this就是ThreadLocal,它作为key
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 这里的key就是ThreadLocal对象,根据它的threadLocalHashCode属性值进行取模计算,得到它应该所在的下标值是多少
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
// 如果所在的下标已经有值了,则根据线性探测继续计算下一个下标值
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
...
}
...
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
根据以上的分析,ThreadLocal对象作为ThreadLocal.ThreadLocalMap中的key存放在Map中.(Map的底层是基于数组)
具体是根据ThreadLocal对象的threadLocalHashCode属性值进行取模计算出它在数组中的下标,假如ThreadLocal的threadLocalHashCode=1253254570,数组的默认长度是16,因此threadLocalHashCode & (16-1)=10,因此这个ThreadLocal对象应该放在数组的第10个位置.如果第10个位置已经有别的ThreadLocal对象霸占了,那么它就会尝试第11个位置是否有空缺,以此类推,直到找到一个空缺位置.
开放定址法中的线性探测
举例如下
import io.netty.util.concurrent.FastThreadLocal;
public class Address {
// 定义了两个ThreadLocal对象
public static final ThreadLocal<String> COMPANY = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "CHINA";
}
};
public static final ThreadLocal<Integer> YEAR = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 1949;
}
};
}
Thread t1 = new Thread(() -> {
System.out.println(Address.COMPANY.get());
System.out.println(Address.YEAR.get());
try {
TimeUnit.MINUTES.sleep(15);// 只是为了让线程不终止
} catch (InterruptedException ignored) { }
}, "thread-1");
t1.start();
Thread t2 = new Thread(() -> {
System.out.println(Address.COMPANY.get());
System.out.println(Address.YEAR.get());
try {
TimeUnit.MINUTES.sleep(15);// 只是为了让线程不终止
} catch (InterruptedException ignored) { }
}, "thread-2");
t2.start();
根据代码和截图内容,我们总结下
线程2的哈希值1253254570的ThreadLocal存在索引10位置
线程2的哈希值-1401181199的ThreadLocal存在索引1位置
线程1的哈希值1253254570的ThreadLocal存在索引10位置
线程1的哈希值-1401181199的ThreadLocal存在索引1位置
JDK的ThreadLocal是根据它的哈希值然后再取模计算出索引位置,如果冲突还要再根据开放地址法-线性探测继续寻找下一个可用索引的位置.性能是比较低的. 那么我们看下在Netty中它是如何优化这个功能的呢?
import io.netty.util.concurrent.FastThreadLocalThread;
FastThreadLocalThread t3 = new FastThreadLocalThread(() -> {
System.out.println(Address.FAST_COMPANY.get());
System.out.println(Address.FAST_YEAR.get());
try {
TimeUnit.MINUTES.sleep(15);
} catch (InterruptedException ignored) {
}
}, "thread-3");
t3.start();
FastThreadLocalThread t4 = new FastThreadLocalThread(() -> {
System.out.println(Address.FAST_COMPANY.get());
System.out.println(Address.FAST_YEAR.get());
try {
TimeUnit.MINUTES.sleep(15);
} catch (InterruptedException ignored) {
}
}, "thread-4");
t4.start();
这段代码使用了Netty提供的FastThreadLocalThread线程,在它的内部有个自己的InternalThreadLocalMap,这个Map是与FastThreadLocal搭配使用的.
FastThreadLocal内部并不像JDK的ThreadLocal是根据哈希值与取模计算索引位置,它是在创建FastThreadLocal的时候就已经确定了索引位置,在JVM中每个FastThreadLocal的索引值都是不同的.
根据索引直接存取值,时间复杂度O(1)
private final int index;
public FastThreadLocal() {
// 创建FastThreadLocal时它的索引值index就确定下来了
index = InternalThreadLocalMap.nextVariableIndex();
}
像Netty这样追求性能的底层网络框架,自己设计一个FastThreadLocalThread线程类,自己设计一个FastThreadLocal类,自己设计一个InternalThreadLocalMap类等.
公众号: Netty历险记