ThreadLocal详解

☆* o(≧▽≦)o *☆嗨~我是小奥
个人博客:小奥的博客
CSDN:个人CSDN
Github:传送门
面经分享(牛客主页):传送门
文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️

文章目录

  • ThreadLocal详解
    • 1. 基本介绍
    • 2. Thread、ThreadLocal、ThreadLocalMap的关系
    • 3. 实现原理
      • ① initialValue()
      • ② get()
      • ③ set(T value)
      • ④ remove
    • 4. ThreadLocalMap
      • ① Entry
      • ② Set()
      • ③ getEntry
      • ④ rehash()
      • ⑤ remove()
    • 5. 内存泄漏

ThreadLocal详解

1. 基本介绍

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例时(通过get和set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段。使用它的目的是希望将状态(例如用户ID或者事务ID)与线程关联起来。这些变量分配在堆内的TLAB中。

ThreadLocal的使用非常简单,只需要在每个线程中调用set()方法来存储数据,然后在需要的时候调用get()方法来获取数据。在多线程环境下,每个线程都拥有自己的ThreadLocal实例,因此可以独立地存储和访问自己的数据,从而避免了线程安全问题。

ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的。

ThreadLoacl作用:

  • 线程并发:应用在多线程并发下的场景
  • 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度
  • 线程隔离:每个线程的变量都是独立的,不会相互影响

对比 synchronized:

synchronized ThreadLocal
原理 同步机制采用以时间换空间的方式,
只提供了一份变量,让不同的线程排队访问
ThreadLocal 采用以空间换时间的方式,
为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

常用方法:

方法 描述
get() 返回当前线程的此线程局部变量副本中的值
set(T value) 将当前线程的此线程局部变量的副本设置为指定的值
remove() 删除此线程局部变量的当前线程的值
initialValue() 返回此线程局部变量的当前线程的“初始值”
withInitial(Supplier) 创建线程局部变量

阿里巴巴规范约定:必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用try-finally块进行回收。

2. Thread、ThreadLocal、ThreadLocalMap的关系

  • 每个Thread线程内部都有一个Map(ThreadLocal.ThreadLocalMap)
  • Map里面存储的是ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map中获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,其他的线程并不能获取当前线程的副本值,所以形成了副本隔离。

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。

ThreadLocal详解_第1张图片

3. 实现原理

① initialValue()

返回该线程局部变量的初始值。

  • 延迟调用的方法,在执行get方法时才调用
  • 该方法缺省(默认)实现直接返回null
  • 如果想要一个初始值,可以重写此方法,该方法是一个protected的方法,就是为了让子类覆盖而设计的
	protected T initialValue() {
	    return null;
	}

② get()

获取当前线程与当前ThreadLocal对象相关联的线程局部变量。

	public T get() {
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // 如果此map存在
	    if (map != null) {
	        // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
	        ThreadLocalMap.Entry e = map.getEntry(this);
	        // 对 e 进行判空 
	        if (e != null) {
	            // 获取存储实体 e 对应的 value值
	            T result = (T)e.value;
	            return result;
	        }
	    }
	    /*有两种情况有执行当前代码
	      第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
	      第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */
	    // 初始化当前线程与当前 threadLocal 对象相关联的 value
	    return setInitialValue();
	}
	private T setInitialValue() {
	    // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null
	    T value = initialValue();
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // 判断 map 是否初始化过
	    if (map != null)
	        // 存在则调用 map.set 设置此实体 entry,value 是默认的值
	        map.set(this, value);
	    else
	        // 调用 createMap 进行 ThreadLocalMap 对象的初始化中
	        createMap(t, value);
	    // 返回线程与当前 threadLocal 关联的局部变量
	    return value;
	}

③ set(T value)

	public void set(T value) {
	    // 获取当前线程对象
	    Thread t = Thread.currentThread();
	    // 获取此线程对象中维护的 ThreadLocalMap 对象
	    ThreadLocalMap map = getMap(t);
	    // 判断 map 是否存在
	    if (map != null)
	        // 调用 threadLocalMap.set 方法进行重写或者添加
	        map.set(this, value);
	    else
	        // map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量
	        createMap(t, value);
	}
	// 获取当前线程 Thread 对应维护的 ThreadLocalMap 
	ThreadLocalMap getMap(Thread t) {
	    return t.threadLocals;
	}
	// 创建当前线程Thread对应维护的ThreadLocalMap 
	void createMap(Thread t, T firstValue) {
	    // 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据
	    t.threadLocals = new ThreadLocalMap(this, firstValue);
	}

④ remove

	public void remove() {
	    // 获取当前线程对象中维护的 ThreadLocalMap 对象
	    ThreadLocalMap m = getMap(Thread.currentThread());
	    if (m != null)
	        // map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体
	        m.remove(this);
	}

4. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;
// 存放数据的table,数组长度必须是2的整次幂。
private Entry[] table;
// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值
private int size = 0;
// 进行扩容的阈值,表使用量大于它的时候进行扩容。
private int threshold;

① Entry

  • Entry继承WeakReference,key是弱引用,目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
  • Entry限制只能使用ThreadLocal作为key,key为null(entry.get() == null ) 意味着不再被引用,entry也可以从table中清除。
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        // this.referent = referent = key;
        super(k);
        value = v;
    }
}

② Set()

添加数据,ThreadLocalMap使用线性探测法来解决哈希冲突。

  • 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍。

    假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组

  • 线性探测法会出现堆积问题,可以采取平方探测法。

  • 在探测过程中ThreadLocal会复用key为null的脏Entry对象,并进行垃圾清理,防止出现内存泄漏。

private void set(ThreadLocal<?> key, Object value) {
    // 获取散列表
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode & (len-1);
    // 使用线性探测法向后查找元素,碰到 entry 为空时停止探测
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取当前元素 key
        ThreadLocal<?> k = e.get();
        // ThreadLocal 对应的 key 存在,【直接覆盖之前的值】
        if (k == key) {
            e.value = value;
            return;
        }
        // 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】
        
        // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】
        if (k == null) {
            // 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】
            // 这个方法还进行了垃圾清理动作,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry
    tab[i] = new Entry(key, value);
    // 数量 + 1
    int sz = ++size;
    
    // 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 扩容
        rehash();
}
// 获取【环形数组】的下一个索引
private static int nextIndex(int i, int len) {
    // 索引越界后从 0 开始继续获取
    return ((i + 1 < len) ? i + 1 : 0);
}
// 在指定位置插入指定的数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
	// 探测式清理的开始下标,默认从当前 staleSlot 开始
    int slotToExpunge = staleSlot;
    // 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值
    // 【保证在一个区间段内,从最前面的过期数据开始清理】
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

	// 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 获取当前节点的 key
        ThreadLocal<?> k = e.get();
		// 条件成立说明是【替换逻辑】
        if (k == key) {
            e.value = value;
            // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致
            // 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry
            // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置,
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
			
            // 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            
            // 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
		// 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据
        if (k == null && slotToExpunge == staleSlot)
            // 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据
            slotToExpunge = i;
    }
	// 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】
    // 删除原有的数据引用,防止内存泄露
    tab[staleSlot].value = null;
    // staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】
    tab[staleSlot] = new Entry(key, value);

    // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

ThreadLocal详解_第2张图片

private static int prevIndex(int i, int len) {
    // 形成一个环绕式的访问,头索引越界后置为尾索引
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

③ getEntry

ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e。

private Entry getEntry(ThreadLocal<?> key) {
    // 哈希寻址
    int i = key.threadLocalHashCode & (table.length - 1);
    // 访问散列表中指定指定位置的 slot 
    Entry e = table[i];
    // 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 进行线性探测
        return getEntryAfterMiss(key, i, e);
}
// 线性探测寻址
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 获取散列表
    Entry[] tab = table;
    int len = tab.length;
    // 开始遍历,碰到 slot == null 的情况,搜索结束
    while (e != null) {
		// 获取当前 slot 中 entry 对象的 key
        ThreadLocal<?> k = e.get();
        // 条件成立说明找到了,直接返回
        if (k == key)
            return e;
        if (k == null)
             // 过期数据,【探测式过期数据回收】
            expungeStaleEntry(i);
        else
            // 更新 index 继续向后走
            i = nextIndex(i, len);
        // 获取下一个槽位中的 entry
        e = tab[i];
    }
    // 说明当前区段没有找到相应数据
    // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数
    return null;
}

④ rehash()

触发一次全量清理,如果数组长度大于等于长度的1/2,则进行resize。

private void rehash() {
    // 清楚当前散列表内的【所有】过期的数据
    expungeStaleEntries();
    
    // threshold = len * 2 / 3,就是 2/3 * (1 - 1/4)
    if (size >= threshold - threshold / 4)
        resize();
}
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);
    }
}

Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新数组的长度是老数组的二倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    // 统计新table中的entry数量
    int count = 0;
	// 遍历老表,进行【数据迁移】
    for (int j = 0; j < oldLen; ++j) {
        // 访问老表的指定位置的 entry
        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);
                // 将数据存放到新表合适的 slot 中
                newTab[h] = e;
                count++;
            }
        }
    }
	// 设置下一次触发扩容的指标:threshold = len * 2 / 3;
    setThreshold(newLen);
    size = count;
    // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用
    table = newTab;
}

⑤ remove()

删除Entry。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 哈希寻址
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 找到了对应的 key
        if (e.get() == key) {
            // 设置 key 为 null
            e.clear();
            // 探测式清理
            expungeStaleEntry(i);
            return;
        }
    }
}

5. 内存泄漏

(1)内存泄漏产生:ThreadLocal 内存泄漏的原因通常是因为ThreadLocalMap 是 Thread的一个属性, ThreadLocal 的生命周期和线程的生命周期一样长。而Entry将ThreadLocal作为key,值作为value对象,它继承自WearReference,并且在构造函数中调用了super()方法,所以ThreadLocal对象的key是一个弱引用,而value是一个强引用。

当GC回收ThreadLocal时,弱引用的key会被回收,但是强引用的value不会被回收,就会造成内存泄漏。

主要有两个原因:

  • 没有手动删除这个Entry
  • 当前线程依然继续运行

关于第一点,只要在使用完ThreadLocal,手动调用其remove方法删除对应的Entry,就能避免内存泄漏。

关于第二点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期和线程一样。如果ThreadLocal变量被回收,那么当前线程的ThreadLocal变量副本所指向的key就是null,即Entry的结构为(null,value),那么这个Entry对应的value永远无法被访问到,而value还存在线程的强引用,只有在线程退出以后,value的强引用才会断开。

(2)如何解决内存泄漏

  • 及时清除 ThreadLocal 变量副本:在使用完 ThreadLocal 后,及时调用 remove() 方法删除对应线程的变量副本,避免造成内存泄漏。可以考虑使用 try-finally 语句块来确保在方法结束时一定会调用 remove() 方法。
  • 使用弱引用的 ThreadLocal 实例:可以使用 WeakReference 来包装 ThreadLocal 实例,这样 ThreadLocal 实例会被视为弱引用,当没有强引用指向 ThreadLocal 实例时,ThreadLocal 实例就会被垃圾回收。
  • 使用线程池或者线程复用机制:在一些情况下,可以使用线程池或者线程复用机制来避免频繁地创建和销毁线程,从而降低 ThreadLocal 内存泄漏的风险。

(3)为什么不把Key设置为强引用

如果当ThreadLocalMap的key为强引用,当垃圾回收时,由于ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除的话,那么ThreadLocal不会被回收,导致Entry内存泄漏。

通过使用 WeakReference 来包装 ThreadLocal 实例,可以让 ThreadLocal 实例变为弱引用,当没有其他强引用指向 ThreadLocal 实例时,ThreadLocal 实例就会被垃圾回收。因此,使用 WeakReference 可以避免 ThreadLocal 实例的内存泄漏。

由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set()、getEntry()、remove()方法的时候会通过线性探测法堆key进行判断,如果key为null(即ThreadLocal为null)则会对Entry进行垃圾回收。所以使用弱引用比强引用多一层保障,就算不调用 remove,也有机会进行 GC。

你可能感兴趣的:(Java并发源码,Java,ThreadLocal)