弄懂 ThreadLocal,看这一篇就够了

1 什么是 ThreadLocal?

ThreadLocal 类用于提供线程内部的局部变量,变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。

ThreadLocal 有几个常用的方法,分别为 set(存储),get(获取),remove(删除),下面我会对这几个方法分别进行介绍。

2 set 方法

我们如何设置当前线程对应的值呢?通过 set 方法即可。

public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

getMap 方法的实现如下:

ThreadLocalMap getMap(Thread t) {
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
  }

createMap 方法的实现如下:

void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在上面,我们可以发现,每一个线程都持有一个 ThreadLocalMap 对象,如果该对象未被实例化则就将其实例化且赋值给成员变量 threadLocals,否则就直接使用已经实例化的对象,然后将对 ThreadLocal 的操作转化为对 ThreadLocalMap 对象的操作。

每一个线程都持有一个 ThreadLocalMap 对象,从 Thread 的源码来看,确实如此。以下代码是在 Thread 中对于 ThreadLocalMap 的声明。

ThreadLocal.ThreadLocalMap threadLocals = null;

3 ThreadLocalMap

我们上面提到过,对 ThreadLocal 的操作最终会转化为对 ThreadLocalMap 的操作,我们接下来就来学习一下 ThreadLocalMap 的操作。

Entry

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

    Entry(ThreadLocal<?> k, Object v) {
           super(k);
            value = v;
    }
}

Entry 为 ThreadLocalMap 的静态内部类,也是对 ThreadLocal 的弱引用,使 ThreadLocal 和储值形成 key-value 的关系。

构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //内部成员数组,INITIAL_CAPACITY值为16的常量
        table = new Entry[INITIAL_CAPACITY];
        //位运算,结果与取模相同,计算出需要存放的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

可见,在实例化 ThreadLocalMap 时,创建了一个长度为16的 Entry 数组,然后通过 hashCode 与 length 位运算确定了一个索引值 i,这个 i 就是元素被存储在 table 数组中的位置。

我们之前说过,ThreadLocal 的操作在底层被转化为对 ThreadLocalMap 的操作,且每个线程内部实现了一个 ThreadLocalMap 类型的实例 threadLocals。在这里,我们发现,ThreadLocalMap 其实内部维护了一个数组,即每个线程内部维护了一个数组,一切操作,都是通过对数组的操作来实现的。

在一个线程内声明多个 ThreadLocal

如果我们在一个线程内声明多个 ThreadLocal,由于一个线程只维护一个ThreadLocalMap,所以这多个 ThreadLocal 对应了一个 ThreadLocalMap 对象,那么我们应该如何在一个 ThreadLocalMap 管理这多个 ThreadLocal 呢?

由于 ThreadLocalMap 的底层实现为数组,我们便自然而然地想到把多个 ThreadLocal 存放到数组的不同位置即可。那么问题来了,这多个 ThreadLocal 在数组中的位置是如何确定的呢?为了能够正确访问,我们需要有一种方法来计算 ThreadLocal 在数组中的索引值。那么,接下来我们便来看看在 ThreadLocalMap 的 set 方法中是如何计算索引值的。

  		//ThreadLocalMap中set方法
  		private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            //获取索引值
            int i = key.threadLocalHashCode & (len-1);

            //遍历tab 如果已经存在则更新值
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //如果上面没有遍历成功则创建新值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //满足条件数组扩容x2
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

我们看一下获取索引值的代码,int i = key.threadLocalHashCode & (len-1);,其中 threadLocalHashCode 位于 ThreadLocal 中,相关代码如下:

public class ThreadLocal<T> {
	···
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
    	//自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
	···

}

在对 ThreadLocal 进行实例化时,会使 threadLocalHashCode 值自增一次,增量为 0x61c88647。为什么是 0x61c88647 而不是其他数呢?其实 0x61c88647 是斐波那契散列乘数,其优点为通过它散列出来的结果分布会比较均匀,可以很大程度上避免哈希冲突。

ThreadLocalMap 的底层实现

ThreadLocalMap 的底层是一个 HashMap 哈希表。核心元素包括:

  1. Entry[] table:必要时需要扩容,长度必须是2的 n 次方
  2. int size:实际存储键值对元素个数
  3. int threshold:下一次扩容时的阈值,threshold 为 table 长度的 2/3。当 size >= threshold 时,遍历 table 并删除 key 为 null 的元素,如果删除后 size >= threshold * 3/4 时,需要对 table 进行扩容

由 Entry[] table 可见,哈希表存储的核心元素是 Entry,Entry 包括:

  1. ThreadLocal<?> k:当前存储的 ThreadLocal 实例对象
  2. Object value:当前 ThreadLocal 储存的值 value

在上面代码中可见,Entry 继承了弱引用 WeakReference。在使用 ThreadLocalMap 时,如果 key 为 null,便说明该 key 对应的 ThreadLocal 不再被引用,需要将其从 ThreadLocalMap 中移除。

为什么 ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key?

如果一个 ThreadLocal 没有外部强引用来引用它,那么在 GC 的时候,这个 ThreadLocal 会被回收,从而导致 ThreadLocalMap 出现 key 为 null 的 Entry,且无法访问这些 key 为 null 的 Entry 的 value。只要当前线程不死亡,这些 value 就会一直存在引用,永远无法被回收,从而造成内存泄漏。

事实上,在 ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的 get,set,remove 等方法调用的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value,但这些被动的预防措施并不能保证不会内存泄漏。

如果 key 使用强引用,在引用的 ThreadLocal 的对象被回收之后,ThreadLocalMap 还持有 ThreadLocal 的强引用,只要没有手动删除,ThreadLocal 就不会被回收,导致 Entry 内存泄漏。

如果 key 使用弱引用,在引用的 ThreadLocal 的对象被回收之后,ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 get,set,remove 等方法的时候会被清除。

比较上面两种情况,可以发现由于 ThreadLocalMap 的生命周期跟 Thread 一样,如果没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 get,set,remove 等方法的时候会被清除。

综上所述,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

那么我们应该如何避免内存泄漏呢?在每次使用完 ThreadLocal 之后,都调用它的 remove 方法,清除数据,就像每次使用完锁就解锁一样。

4 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;
        }
    }
    return setInitialValue();
}

setInitialValue 方法用于初始化操作。

getEntry 方法实现如下:

private Entry getEntry(ThreadLocal<?> key) {
       int i = key.threadLocalHashCode & (table.length - 1);
       Entry e = table[i];
       if (e != null && e.get() == key)
            return e;
       else
            return getEntryAfterMiss(key, i, e);
}

无非是通过计算出索引值到数组相应的地址去寻找数据罢了。

5 remove 方法

	public void remove() {
         //获取ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
         //如果map存在
         if (m != null)
             //以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

该方法可以删除 ThreadLocal 中对应当前线程已存储的值。

6 ThreadLocal 的应用场景

Spring 使用 ThreadLocal 来解决线程安全问题。

一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton (单例)作用域。就是因为 Spring 对一些 Bean 中非线程安全状态采用 ThreadLocal 进行处理,使它们成为线程安全的状态。

7 总结

对于同一 ThreadLocal 来说,在不同线程之间访问的是不同的 table 数组,而且这些线程的 table 数组是互相独立的;对于同一线程的不同 ThreadLocal 来说,它们共享一个 table 数组,每个 ThreadLocal 实例在数组中的位置是不同的。

ThreadLocal 与 Synchronized 均可解决多线程并发访问变量问题,它们区别在于:

  1. Synchronized 牺牲了时间来解决访问冲突,采取了线程阻塞的方法,提供一份变量,让不同的线程排队访问
  2. ThreadLocal 牺牲了空间来解决访问冲突,线程隔离,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响

参考:ThreadLocal
JAVA并发-自问自答学ThreadLocal

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章