告诉你一个将ThreadLocal性能提升三倍的秘密

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实例的不断创建,有内存溢出的风险。


你可能感兴趣的文章:

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__0809 返回首页