ThreadLocal浅析

435 阅读4分钟

这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

一、ThreadLocal的实现原理

Thread有一个内部变量ThreadLocal.ThreadLocalMap,这个类是ThreadLocal的静态内部类,它的实现与HashMap类似,当线程第一次调用ThreadLocalget/set方法时会初始化它。它的键是这个ThreadLocal对象本身,值是需要存储的变量。也就是说ThreadLocal类型的本地变量是存放在具体的线程空间里。当不断的使用get方法获取时,是到线程独有线程空间中获取变量,使得其他线程无法访问到,也就达到了线程安全的目的。在使用完成之后,可以通过remove方法,移除不使用的本地变量。

ThreadLocal和同步机制的比较

​ 如果说同步机制是一种以时间换空间的做法,那么ThreadLocal就是一种以空间换时间的做法,在同步机制下,当访问共享变量时,同步机制保证了同一个时刻只能有一个线程能访问到资源,其他线程会进入阻塞状态。而使用ThreadLocal,为每个线程都复制了共享变量的副本,也就不存在共享变量的说法。

二、源码

1.set方法

通过ThreadLocal的set方法调用到ThreadLocal.ThreadLocalMap静态内部类的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对象流程。

void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  1. 先通过hashcode作为下标取数组对应位置的值,若为空,设置值。若不为空,往后移动一个位置,如果获取到的长度等于数组长度,从0位置查找。
  2. 清除Entry对象还在,但是Entry的值为空的位置 && 当前数量是否大于容量扩容
  static class ThreadLocalMap {
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // 1.
        for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            if (k == key) {
                e.value = value;
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 2.
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

这边的扩容有两个步骤:

  1. 重新排列table数组里的值,根据hashcode获取下标,若对应下标为空,则移动到该位置若下标位置不为空,往后移动位置,直到找到空位置。
  2. 排列的同时如果是空位置,会相应减少size,若排列之后的size仍然大于容量的3/4则扩容
private void rehash() {
  //1. 
  expungeStaleEntries();
  // 2.
  if (size >= threshold - threshold / 4)
  // 两倍原长度扩容
  resize();
}
2.get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        T result = (T)e.value;
        return result;
      }
    }
    // 默认值
    return setInitialValue();
}

获取不到值有两种情况,e=null, e.get() == null,如果e=null直接返回null,如果e.get()=null,清除这个位置的值。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  Entry[] tab = table;
  int len = tab.length;
  while (e != null) {
    ThreadLocal<?> k = e.get();
    if (k == key)
      return e;
    if (k == null)
      expungeStaleEntry(i);
    else
      i = nextIndex(i, len);
    e = tab[i];
  }
  return null;
}

三、内存泄漏

ThreadLocal.png

ThreadLcal的引用关系如上如所示,虚线是使用软引用的地方。现在假设这个地方使用的是强引用,在业务代码中使用threadlocalInstance==nullThreadLocalRefThreadLocal之间的强引用置空,value还是会通过另一条引用链currentThread->currentThread->map->entry->value到达,也是不会被GC掉。而若采用软引用,在系统将要发生内存溢出时会回收掉,也就是会断掉keyThreadLocal之间的引用,使得key=null

​ 在ThreadLocal的实现中,为了避免内存泄漏已经做了很多安全性的控制,在get()set()方法中都有相应的处理,通过特定的方式对存在key=null的脏Entry进行value=null的处理,使得value的引用链不可达。

为什么使用弱引用?

一是尽管使用强引用也会出现内存泄漏,二是在ThreadLocal的生命周期中set、getEntry、remove里,都针对键为空的脏Entry进行处理。但是尽管如此,在编程过程中,形成一种良好的规范,在使用完ThreadLocal后都应该手动调用remove方法进行清理。