ThreadLocal作为线程间数据隔离的工具类,应用场景还是很丰富的,例如:Session的管理、参数的隐式传递、事务的管理等等。笔者前面写过关于ThreadLocal源码解析的文章,感兴趣的同学可以前去阅读:ThreadLocal源码解析。
在那篇文章中,有说到过ThreadLocal是如何处理哈希冲突的。它并没有采用HashMap的链地址法,而是采用了「线性探测」技术。
线性探测
哈希表中已经插入8、9元素,此时再插入14,下标2已经被8给占用了,出现哈希冲突。
线性探测会环形寻找next节点,先找到下标3,被9占用了,依然冲突,再找到下标4,没有被占用,即没有发生冲突,则将14放入下标4的节点中。
查询也是一样的流程,先通过哈希码计算的下标判断Key是否相等,如果不等则寻找下一个,直到找到Key相等的节点,如果遇到节点为null的元素还没有找到,说明Key不存在。
「线性探测」的算法使用一个数组就可以存放所有的元素,不像HashMap还要转换成链表或红黑树。查询的效率要高一丢丢,但是扩容会更频繁一下,相较于HashMap的链地址法,线性探测需要一块更大的连续内存,所以对内存的要求会高一些。
ThreadLocal性能测试
使用线性探测技术,在哈希冲突比较多的情况下,读写的性能会受到影响。
查询时,如果发生哈希冲突,就需要循环访问下一个节点,增加了寻址次数,降低了查询的性能。
如下测试代码,创建十万个ThreadLocal实例,多次遍历:
public static void threadLocalTest(){
ThreadLocal[] locals = new ThreadLocal[100000];
for (int i = 0; i < locals.length; i++) {
locals[i] = new ThreadLocal();
locals[i].set(i);
}
long t1 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
for (ThreadLocal local : locals) {
local.get();
}
}
System.out.println(System.currentTimeMillis() - t1);
}
耗时:6623ms。
优化ThreadLocal
既然知道了ThreadLocal的性能损耗主要是在哈希冲突的处理方式上,那么要优化也就不难了。
永远都不会发生哈希冲突的哈希表
给每个ThreadLocal实例分配一个全局唯一且递增的索引index
,每个线程内部维护一个哈希表,读写数据时,直接根据index哈希表中定位,速度超级快。
缺点就是非常占用内存空间,随着ThreadLocal实例的不断创建,index不断变大,有内存溢出的风险,空间换时间。
说完理论,下面看代码实现:
/**
* @author: pch
* @description: 我的ThreadLocal实现
* @date: 2020/11/25
**/
public class MyThreadLocal<T> {
// 全局递增的索引
static final AtomicInteger NEXT_INDEX = new AtomicInteger(0);
// 每个MyThreadLocal实例都有一个递增的索引
private int index = NEXT_INDEX.getAndIncrement();
// 获取数据
public T get(){
Thread thread = Thread.currentThread();
// 必须配合MyThread使用
if (thread instanceof MyThread) {
return (T) ((MyThread)thread).threadLocalMap.get(this);
}
return null;
}
// 设置数据
public void set(T t){
Thread thread = Thread.currentThread();
// 必须配合MyThread使用
if (thread instanceof MyThread) {
((MyThread) thread).threadLocalMap.set(this, t);
return;
}
}
public void remove(){
}
// 自定义的ThreadLocalMap,永远不会出现哈希冲突,空间换时间。
static class ThreadLocalMap {
// 哈希表
Object[] table = new Object[32];
// 查询数据,直接通过index获取,时间复杂度O(1)
public Object get(MyThreadLocal threadLocal) {
return table[threadLocal.index];
}
// 设置数据,不存在哈希冲突
public void set(MyThreadLocal threadLocal,Object value) {
int index = threadLocal.index;
table[index] = value;
// 临界点扩容
if (index >= table.length - 1) {
// 扩容 双倍
table = Arrays.copyOf(table, table.length * 2);
}
}
}
// 自定义线程,MyThreadLocal必须配合MyThread使用
public static class MyThread extends Thread {
ThreadLocalMap threadLocalMap = new ThreadLocalMap();
public MyThread(Runnable target) {
super(target);
}
}
}
篇幅原因,只贴出比较重要的核心代码。
和前面ThreadLocal一样的测试标准,对优化后的MyThreadLocal做一下性能测试:
public static void myThreadLocalTest(){
// 必须配合MyThread使用
new MyThreadLocal.MyThread(()->{
MyThreadLocal[] locals = new MyThreadLocal[100000];
for (int i = 0; i < locals.length; i++) {
locals[i] = new MyThreadLocal();
locals[i].set(i);
}
long t1 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
for (MyThreadLocal local : locals) {
local.get();
}
}
System.out.println(System.currentTimeMillis() - t1);
}).start();
}
耗时:1613ms。
可以看到,性能提升了三倍多,缺点就是耗内存,没办法,空间换时间嘛。
FastThreadLocal
为什么会写这篇文章呢,其实就是因为看了Netty的代码,里面有一个io.netty.util.concurrent.FastThreadLocal
类。
从名字就可以看出来,是一个比ThreadLocal性能更好的工具类。刚开始看到的时候觉得很疑惑,既然JDK已经提供了ThreadLocal,为什么Netty还要再造轮子呢???
显然是因为Netty觉得JDK提供的ThreadLocal性能太差了,那Netty是如何优化它的呢?
强烈好奇心的驱使下,笔者看了下它的源码。
FastThreadLocal必须配合FastThreadLocalThread使用:
InternalThreadLocalMap是Netty自己实现的用来维护线程和ThreadLocal之间的关系的容器,它继承自UnpaddedInternalThreadLocalMap。
由于每个FastThreadLocal实例都有自己的index,因此在写数据的时候,直接往哈希表的index位置写就行了,不存在哈希冲突。
查询数据时,也是直接根据index快速定位:
FastThreadLocal的优点是数据的读写速度极快,不存在哈希冲突。缺点也很明显,扩容会更加频繁,对内存的要求非常高,随着FastThreadLocal实例的不断创建,有内存溢出的风险。
你可能感兴趣的文章: