【JavaSE】多线程(5)_ThreadLocal及其常用方法set()、get()、remove()详解

本篇总结ThreadLocal的相关知识,它在实际开发中使用频率较高,要认真学 ~

1 ThreadLocal

  • ThreadLocal是一种类型,用于提供 线程局部变量,在多线程环境下可以 保证各个线程里的变量独立于其他线程的变量
  • 即:ThreadLocal可为每个线程创建一个单独的变量副本,相当于线程的private static类型变量。
  • 在多线程环境下,同步机制是为了保证数据的一致性,而ThreadLocal是为了保证数据的独立性。

2 ThreadLocal实现

public void set(T value)
public T get()
public void remove()
protected T initialValue()
  • set(T value)方法

    • 首先获取当前线程,然后再获取到当前线程的ThreadLocalMap,若它不为null,则将value保存其中,并用当前的ThreadLocal作为key;否则创建一个ThreadLocalMap并给到当前线程,然后保存value。
    • ThreadLocalMap相当于一个HashMap,是真正保存值的地方。
  • get()方法

    • 在 get() 方法中也会获取到当前线程的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则获取 key 为当前 ThreadLocal 的值;
    • 否则调用 setInitialValue() 方法返回初始值,并保存到新创建的 ThreadLocalMap 中。
  • remove()方法

    • 删除当前key

3 ThreadLocalMap

  • 在 set,get,initialValue 和 remove 方法中都会获取到当前线程,然后通过当前线程获取到 ThreadLocalMap,如果ThreadLocalMap 为 null,则会创建一个 ThreadLocalMap,并给到当前线程。

  • 可见,每个线程都会持有一个ThreadLocalMap用来维护线程本地的值。

  • 每个线程的ThreadLocalMap 是属于线程自己的, ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。

  • 构造方法
    ThreadLocal 中当前线程的 ThreadLocalMap 为 null 时会使用ThreadLocalMap 的构造方法新建一个ThreadLocalMap:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
	table = new Entry[INITIAL_CAPACITY];
	int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
	//在构造方法中新建一个数组,并将第一次需保存的键值存储到一个数组中,完成初始化。
	table[i] = new Entry(firstKey, firstValue);
	size = 1;
	setThreshold(INITIAL_CAPACITY);
}
  • 存储结构
    ThreadLocalMap 内部维护一个哈希表(数组)来存储数据,并且定义了加载因子。
// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储数据的哈希表
//这个table是一个Entry类型的数组,Entry是ThreadLocalMap的一个内部类。
//Entry 用于保存一个键值对,其中key以“弱引用”的方式保存。
private Entry[] table;
// table 中已存储的条目数
private int size = 0;
// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;
// 设置 threshold 的值
private void setThreshold(int len){
	threshold = len *2/3;
}
  • 保存键值对

    调用set(ThreadLocal key,Object value)方法将数据保存到哈希表中。

private void set(ThreadLocal key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算要存储的索引位置
	int i = key.threadLocalHashCode & (len-1);
	// 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
	for (Entry e = tab[i];
	e != null;
	e = tab[i = nextIndex(i, len)]) {
		ThreadLocal k = e.get();
		// 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
		if (k == key) {
			e.value = value;
			return;
		}
		// 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
	// 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
	tab[i] = new Entry(key, value);
	// 增加 table 存储的条目数
	int sz = ++size;
	// 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}
  • 首先使用 key(当前 ThreadLocal)的 threadLocalHashCode 来计算要存储的索引位置 i。threadLocalHashCode 的值由ThreadLocal 类管理,每创建一个 ThreadLocal 对象都会自动生成一个相应的 threadLocalHashCode 值。

  • 在保存数据时,如果索引位置有 Entry,且该 Entry 的 key 为 null,就会清除无效 Entry 来腾出空间,因为:Entry的 key 是弱引用,key 一旦被回收(即 key 为 null),就无法再访问到 key 对应的 value。

  • 在调整 table 容量时,也会先清除无效对象,然后再根据需要扩容。

  • 获取Entry对象

    • 使用getEntry(ThreadLocal key)方法。
    • 因为可能存在哈希冲突,key对应的Entry的存储位置可能不在由key计算出的索引位置上,所以要调用getEntryAfterMiss(ThreadLocal key,int i,Entry e)方法获取。
  • 内存泄露

    • 在ThreadLocalMap的set、get、remove方法中都有清除无效Entry的操作,和Entry的key使用弱引用的方式,都是为了降低内存泄漏的概率,但不能完全避免。
    • 开发中每次用完ThreadLocal 都要调用remove方法,清除所有key为null的value,防止内存泄漏。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章