首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

ThreadLocal源码解析

  • 23-11-17 19:42
  • 2302
  • 5630
blog.csdn.net

Java并发编程的学习过程中,一定绕不过ThreadLocal,在实际开发中,ThreadLocal的应用场景还是很丰富的:

  1. 线程间数据的隔离。
  2. Session的管理。
  3. 事务的管理。
  4. 参数的隐式传递(PageHelper)。
  5. Dubbo的RpcContext。

为了更好的理解ThreadLocal原理,笔者记录一下源码阅读的过程,错误之处,还望读者指出,不胜感激。


源码解析

1、threadLocalHashCode
ThreadLocal实例会被当做Key存放到Thread的ThreadLocalMap中,因此需要根据ThreadLocal的hashCode计算一个下标,还要解决哈希冲突等问题。ThreadLocalMap并不是根据hashCode()方法来计算哈希值,而是用了一套递增的规则:

/*
ThreadLocal实例会被当做Key存放到Thread的ThreadLocalMap中。
需要根据hashCode来计算下标。
这里并没有调用hashCode()方法,而是根据0x61c88647的步长一直递增计算的。
 */
private final int threadLocalHashCode = nextHashCode();

// 通过CAS的方式来生成hashCode
private static AtomicInteger nextHashCode =
		new AtomicInteger();

// hashCode递增的步长,为什么是这个数?https://zhuanlan.zhihu.com/p/40515974
private static final int HASH_INCREMENT = 0x61c88647;

// 计算下一个hashCode,一直递增
private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

递增的步长为什么是0x61c88647?
ThreadLocalMap底层是用Entry[]实现的,和HashMap一样,这个数组的长度不管如何扩容,始终都会是2的N次方,以0x61c88647为步长做递增,可以让hashCode更加均匀的分布在2的N次方的数组里。具体可以参考:从 ThreadLocal 的实现看散列算法。

2、set()做了什么?
当线程调用ThreadLocal的set()方法时,它首先会获取当前线程的ThreadLocalMap,如果为null,则创建一个ThreadLocalMap,否则往ThreadLocalMap里put元素。

/*
1.获取当前线程的ThreadLocalMap
2.为null则创建,并set
3.不为null则直接set
 */
public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

一个新的线程首次set()时,会创建一个ThreadLocalMap:

/*
给Thread的threadLocals创建Map实例,并添加元素。
 */
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ThreadLocalMap
在这里插入图片描述
ThreadLocalMap是ThreadLocal的静态内部类,底层采用Entry[]数组来保存数据。和HashMap不同的是,遇到哈希冲突时,Entry并不会转换为链表或红黑树,而是采用开放定址法的线性探测来实现的。
关于哈希冲突的处理方式有哪些,可以看笔者的另一篇文章:哈希冲突的常见解决方式。

// 初始化ThreadLocalMap实例
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
	// 初始化,默认容量16
	table = new Entry[INITIAL_CAPACITY];
	// 计算下标,算法:hashCode & (len - 1),和HashMap一样,这里不详叙。
	int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
	table[i] = new Entry(firstKey, firstValue);
	size = 1;
	// 设置扩容阈值:容量的三分之二
	setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {
	threshold = len * 2 / 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Entry
Entry继承自WeakReference,它的Key是一个弱引用,使用的时候需要注意,尽可能的去手动执行remove(),以免发生内存泄漏。

/*
Entry的Key是弱引用。
当ThreadLocal实例外部不存在强引用时,GC就会将其回收掉。
如果没有调用remove(),value就仍然还有引用,没法回收。
这时就容易导致内存泄漏。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
	Object value;

	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如果ThreadLocalMap不为null时,则需要往里面添加元素了:

private void set(ThreadLocal<?> key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算下标,算法:hashCode & (len - 1),和HashMap一样,这里不详叙。
	int i = key.threadLocalHashCode & (len-1);

	for (Entry e = tab[i];
		 /*
		 如果下标元素不是null,有两种情况:
		 1.同一个Key,覆盖value。
		 2.哈希冲突了。
		  */
		 e != null;
		 /*
		 哈希冲突的解决方式:开放定址法的线性探测。
		 当前下标被占用了,就找next,找到尾巴还没找到就从头开始找。
		 直到找到没有被占用的下标。
		  */
		 e = tab[i = nextIndex(i, len)]) {
		ThreadLocal<?> k = e.get();

		if (k == key) {
			// 相同的Key,则覆盖value。
			e.value = value;
			return;
		}

		if (k == null) {
			/*
			下标被占用,但是Key.get()为null。说明ThreadLocal被回收了。
			需要进行替换。
			 */
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	tab[i] = new Entry(key, value);
	int sz = ++size;
	/*
	1.判断是否可以清理一些槽位。
	2.如果清理成功,就无需扩容了,因为已经腾出一些位置留给下次使用。
	3.如果清理失败,则要判断是否需要扩容。
	 */
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

元素添加成功后,ThreadLocalMap会对元素中已经被回收的Key做清理工作。
处于性能考虑,ThreadLocalMap并不会对所有的元素进行检查,而是采样部分数据。

/*
清理部分槽位。
1.如果清理成功,就不用扩容了,因为已经腾出一部分位置了。
2.处于性能考虑,不会做所有元素做清理工作,而是采样清理。

set()时,n=size,搜索范围较小。
 */
private boolean cleanSomeSlots(int i, int n) {
	boolean removed = false;
	Entry[] tab = table;
	int len = tab.length;
	do {
		i = nextIndex(i, len);
		Entry e = tab[i];
		if (e != null && e.get() == null) {
			// 一旦搜索到了过期元素,则n=len,扩大搜索范围
			n = len;
			removed = true;
			// 真正清理的逻辑
			i = expungeStaleEntry(i);
		}
		/*
		采样规则: n >>>= 1 (折半)
		例:100 > 50 > 25 > 12 > 6 > 3 > 1
		 */
	} while ( (n >>>= 1) != 0);
	return removed;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

如果找到了过期的Key,那就要进行清理工作了:

/*
删除过期的元素:占用下标,但是ThreadLocal实例已经被回收的元素。
 */
private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// 清理当前Entry
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--;

	// Rehash until we encounter null
	Entry e;
	int i;
	// 继续往后寻找,直到遇到null结束
	for (i = nextIndex(staleSlot, len);
		 (e = tab[i]) != null;
		 i = nextIndex(i, len)) {
		ThreadLocal<?> k = e.get();
		if (k == null) {
			// 再次发现过期元素,清理掉
			e.value = null;
			tab[i] = null;
			size--;
		} else {
			// 处理重新哈希的逻辑
			int h = k.threadLocalHashCode & (len - 1);
			if (h != i) {
				tab[i] = null;

				// Unlike Knuth 6.4 Algorithm R, we must scan until
				// null because multiple entries could have been stale.
				while (tab[h] != null)
					h = nextIndex(h, len);
				tab[h] = e;
			}
		}
	}
	return i;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

清理时,并不是只清理掉当前Entry就结束了,而是会往后环形的继续寻找过期的Entry,只要找到了就清理,直到遇到tab[i]==null就结束,清理的过程中还会对元素做一个rehash的操作。

如果清理不成功,则要判断size是否超过threshold阈值,如果超过,则要进行全量的清理工作和判断是否扩容。

private void rehash() {
	// 全量清理过期Entry
	expungeStaleEntries();

	// 清理后,如果size依然超过阈值的四分之三,则要扩容
	if (size >= threshold - threshold / 4)
		resize();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

全量清理过期Entry:

// 全量清理过期Entry
private void expungeStaleEntries() {
	Entry[] tab = table;
	int len = tab.length;
	for (int j = 0; j < len; j++) {
		Entry e = tab[j];
		// 遍历数组,找到过期元素就清理
		if (e != null && e.get() == null)
			expungeStaleEntry(j);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

清理后,如果size依然超过阈值的四分之三,则要扩容:

// 扩容规则:双倍扩容
private void resize() {
	Entry[] oldTab = table;
	int oldLen = oldTab.length;
	int newLen = oldLen * 2;
	Entry[] newTab = new Entry[newLen];
	int count = 0;

	for (int j = 0; j < oldLen; ++j) {
		Entry e = oldTab[j];
		if (e != null) {
			ThreadLocal<?> k = e.get();
			if (k == null) {
				// 扩容期间发现过期元素,会跳过
				e.value = null; // Help the GC
			} else {
				// 将旧数组中没有过期的元素挪到新数组里
				int h = k.threadLocalHashCode & (newLen - 1);
				while (newTab[h] != null)
					h = nextIndex(h, newLen);
				newTab[h] = e;
				count++;
			}
		}
	}
	// 重新设置阈值
	setThreshold(newLen);
	size = count;
	table = newTab;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

至此,Set()的逻辑全部结束。

3、get()做了什么?
get()获取Value时,首先会判断当前线程的ThreadLocalMap是否为null,如果为null,则会调用initialValue()获得一个初始值,并set()到ThreadLocalMap中。

/*
获取Value时:
1.获取当前线程的ThreadLocalMap
2.如果为null,则创建Map并设置初始值。
3.不为null,则通过Map查找。
 */
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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

如果ThreadLocalMap不为null,则要开始查找了:

/*
通过Key获取Entry
 */
private Entry getEntry(ThreadLocal<?> key) {
	// 计算下标
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	if (e != null && e.get() == key) {
		// 如果对应下标节点不为null,且Key相等,则命中直接返回
		return e;
	} else {
		/*
		否则有两种情况:
		1.Key不存在。
		2.哈希冲突了,需要向后环形查找。
		 */
		return getEntryAfterMiss(key, i, e);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

命中则直接返回,不命中有两种情况:

  1. Key不存在。
  2. 哈希冲突了,需要向后环形查找。
/*
无法直接命中的查找逻辑
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
	Entry[] tab = table;
	int len = tab.length;

	while (e != null) {// e==null说明Key不存在,直接返回null
		ThreadLocal<?> k = e.get();
		if (k == key)
			// 找到了,说明是哈希冲突
			return e;
		if (k == null)
			// Key存在,但是过期了,需要清理掉,并且返回null
			expungeStaleEntry(i);
		else
			// 向后环形查找
			i = nextIndex(i, len);
		e = tab[i];
	}
	return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如果成功找到了Entry节点,则直接返回其value即可。

3、remove()做了什么?
获取当前线程的ThreadLocalMap,并删除元素。

// 找到当前线程的ThreadLocalMap,并删除元素
public void remove() {
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null)
		m.remove(this);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

主要逻辑在ThreadLocalMap.remove()里:

// 通过Key删除Entry
private void remove(ThreadLocal<?> key) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算下标
	int i = key.threadLocalHashCode & (len-1);
	/*
	删除也是一样,由于存在哈希冲突,不能直接定位到下标后直接删除。
	删除前需要确认Key是否相等,如果不等需要往后环形查找。
	 */
	for (Entry e = tab[i];
		 e != null;
		 e = tab[i = nextIndex(i, len)]) {
		if (e.get() == key) {
			/*
			找到了就清理掉。
			这里并没有直接清理,而是将Key的Reference引用清空了,
			然后再调用expungeStaleEntry()清理过期元素。
			顺便还可以清理后续节点。
			 */
			e.clear();
			expungeStaleEntry(i);
			return;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

由于哈希冲突的存在,所以不能定位到节点后直接删除,需要确认Key是否相等,如果不等需要往后环形查找,直到找到正确的Key。

清理也不是简单的直接置空,而是先将Key的引用置空,然后调用了expungeStaleEntry()方法清理过期的元素。这个过程会顺带清理后续的节点和rehash操作。


问题

1、为什么要使用弱引用?

每个线程都有自己的ThreadLocalMap,如果ThreadLocalMap强引用了ThreadLocal,那么即使我们执行了ThreadLocal=null,ThreadLocal也无法被回收,难道你想回收ThreadLocal时,遍历所有线程,将所有线程的ThreadLocalMap的当前ThreadLocal进行remove操作???

正是由于采用了弱引用,这才使得,只要ThreadLocal实例外部不存在强引用,GC时就能将其回收,ThreadLocalMap在进行一些读写操作时,也会去自发性的做一些过期检查,删除过期的Entry,最大程度的避免了内存泄漏。

2、为什么会内存泄漏?

ThreadLocalMap的弱引用只针对Key,如果ThreadLocal不存在强引用了,GC就会将其回收,但是Value由于存在和Entry的强引用,因此不会被回收,这样就会导致一些永远也无法被访问的Value存在,即发生内存泄漏。

当然,针对这种情况,JDK已经在尽量去避免了。在对ThreadLocal进行读写时,有很多地方会触发它执行过期检查,删除过期的Entry,避免内存泄漏。

3、什么时候会触发过期检查清理?

  1. 调用set()方法时,采样清理、全量清理,扩容时还会继续检查。
  2. 调用get()方法,没有直接命中,向后环形查找时。
  3. 调用remove()时,除了清理当前Entry,还会向后继续清理。

4、如何避免内存泄漏

使用ThreadLocal时,一般建议将其声明为static final的,避免频繁创建ThreadLocal实例。尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。

ThreadLocal的Value会发生内存泄漏的情况,但是JDK已经做了很多操作来避免。例如上面说的会在很多场景下自发的去清理过期的Entry,使得无效Value可以被回收。一般来说正常使用不会有太大的问题,可能会导致部分Value会发生短暂的内存泄漏,但是在后续的过期检查中,也是会被清理掉的。
尽管如此,还是建议大家及时调用remove()。


你可能感兴趣的文章:

  • AQS源码导读
  • 摊牌了,我要手写一个RPC
  • Java锁的膨胀过程以及一致性哈希对锁膨胀的影响
  • CMS与三色标记算法
  • 大白话理解可达性分析算法
程序员小潘
微信公众号
专注于Java后端技术分享~
注:本文转载自blog.csdn.net的程序员小潘的文章"https://javap.blog.csdn.net/article/details/110006951"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top