ThreadLocal小结

237 阅读5分钟

ThreadLocal

介绍

ThreadLocal 是 JDK 中常用的工具类,它提供了在与当前线程绑定的局部变量,不同线程都会取到不同的值,这在一些并发、变量传递等场景下非常好用。

ThreadLocal 的实现原理:每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本。

通过阅读源码可以发现,这里的底层实现结构为:

  • Thread类中维护了一个ThreadLocalMap
    • 这个ThreadLocalMap是以ThreadLocal为key的
  • ThreadLocal中包含了操作ThreadLocalMap的方法

这样,在操作ThreadLocal的时候,其实是先通过ThreadLocal去获取当前线程,当前线程获取线程中的ThreadLocalMap,然后ThreadLocalMap以当前ThreadLocal为key,就可以获取到数据了。

源码解析

获取元素: ThreadLocal 的 get() 方法是这样的:

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;
        }
    }
    // 如果没有存储过,那就获取初始化value
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
   return t.threadLocals;
}

private T setInitialValue() {
	//默认返回null 可以通过在定义threadlocal的时候 绑定函数化接口
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

    protected T initialValue() {
        return null;
    }

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

设置元素: ThreadLocal 的 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);
}

这里是同样是先获取当前的工作线程,然后拿到线程中的 ThreadLocalMap 对象,同样包含了:

  • 设置
  • 先创建再设置两种逻辑。

ThreadLocalMap

弱引用

这里补充一下弱引用的概念

在Java中,共有四种引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。它们之间的区别和特点如下:

  1. 强引用(Strong Reference):
    • 强引用是最常见的引用类型,也是默认的引用类型。(new出来的)
    • 当一个对象被强引用引用时,即使系统出现内存不足的情况,JVM也不会回收这个对象。
    • 只有当没有任何强引用指向一个对象时,该对象才会被垃圾回收器回收。
  2. 软引用(Soft Reference):
    • 软引用允许对象在内存不足时被回收,但只有在系统认为内存不足时才会回收被软引用引用的对象。
    • 当内存不足时,系统可能会回收被软引用引用的对象,但并不是绝对的。
    • 适合用于实现内存敏感的高速缓存。
  3. 弱引用(Weak Reference):
    • 弱引用比软引用的生命周期更短,只要垃圾回收器运行,只要对象被弱引用引用,对象就可能被回收。
    • 当垃圾回收器运行时,如果一个对象仅被弱引用引用,则该对象会被回收。
    • 一般用于解决内存泄漏问题或者实现对象注册表。
  4. 虚引用(Phantom Reference):
    • 虚引用是最弱的引用类型,主要作用在于跟踪对象被垃圾回收器回收的状态。
    • 当一个对象仅被虚引用引用时,在垃圾回收器将要回收该对象内存时,会将这个对象插入到一个 ReferenceQueue 对象中,以通知应用程序对象即将被回收。
    • 一般用于在对象被回收时执行一些特定操作,比如清理相关资源或进行日志记录。

也就是说,如果一个对象仅被弱引用引用,则该对象会被回收

结构分析

ThreadLocalMap 是一个非常类似于 HashMap 的结构。它以 ThreadLocal 作为 key,value 就是 ThreadLocal 的值。每个线程都有一个 ThreadLocalMap 对象,而 ThreadLocalMap 中保存着当前线程拥有的所有 ThreadLocal。

先展示ThreadLocalMap中的基础结构(entry结构下面再额外展示):

static class ThreadLocalMap {

    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    private int threshold; // Default to 0

    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

    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);
    }

}

可以看到这其实就是一个类似于HashMap的结构,这里多提一嘴,ThreadLocalMap和HashMap的实现还是很不同的。

我们都知道HashMap针对于Hash冲突,采用的解决方案是拉链法,而ThreadLocalMap采用的方法是开放寻址法。

那么我们再聚焦于Entry结构:

static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
        public T get() {
	        return this.referent;
	    }
    }

ThreadLocal 作为 key(是一个弱引用),value 就是对应的值;

这里注意,我们一般实现的Entry结构,都是存储了Object key,Object value,而观看这个Entry类的get方法,可以发现这里的key其实是使用的弱引用。

我们知道直接使用Object key这样的实现方式其实是强引用的,那为什么这里要采用弱引用呢?

答案是:为了防止内存泄露

讨论到为了防止内存泄漏,我们得首先知道为什么会导致内存泄露:

  1. 栈上的ThreadLocal不再需要使用了。
    1. 因为某些原因,我们定义的ThreadLocal对象不再需要使用了,例如方法内的局部变量,那么这个threadlocal就应该被回收
    2. 但是我们也知道,在Thread线程中的ThreadLocalMap中又用了这个ThreadLocal作为key,如果Thread线程不被销毁,那么这个ThreadLocal会一直被持有引用,无法销毁。
    3. 因此,我们这里使用了软引用,当我们的ThreadLocal的栈变量被销毁后,ThreadLocalMap中持有的只是软引用对象,这时候我们的ThreadLocal就只被软引用所持有,就可以被销毁了(上一小节讲过为什么)。
  2. 因为线程池技术,Thread对象并不会被真正的销毁回收。
    1. 那么Thread对象持有的ThreadLocalMap也并不会被刷新回收。
    2. 在Thread复用的情况下,且不进行手动的删除操作,那么ThreadLocalMap中存放的数据将永远不会消除。
    3. 因此,我们一般会要求,在一个逻辑上的线程周期结束之后,需要手动调用ThreadLocal的remove方法来移除该ThreadLocal在该Thread中的数据。