弄懂 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 其实内部维护了一个数组,即每个线程内部维护了一个数组,一切操作,都是通过对数组的操作来实现的。

扫描二维码关注公众号,回复: 9829031 查看本文章

在一个线程内声明多个 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

发布了133 篇原创文章 · 获赞 249 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/104747778