ThreadLocal

1. 简介

ThreadLocal是Thread的局部变量,用于编写多线程程序,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。

能干什么:

  1. 存储数据 & 线程间数据隔离
  2. 在进行对象间跨层传递的时候,使用ThreadLocal可以避免多次传递。比如将用户信息set到ThreadLocal中,当前线程在任何地方都可以通过ThreadLocal对象直接get用户信息,不用为了获取用户信息而把用户信息传来传去

2. 初探

既然说 ThreadLocal 是线程的局部变量,那么一起验证下以下两个问题:

  1. 主线程中set的Hello World ~,在thread-1线程中能否get到
  2. 在thread-1线程中设置的值能否在主线程中get到 & thread-1线程set的值会不会影响主线程中set的结果
@Slf4j
public class ThreadLocalTest {
    final static ThreadLocal<String> tl = new ThreadLocal<>();
    
    @Test
    @SneakyThrows
    public void test() {
        tl.set("Hello World ~");
        log.info("{}:value = {}", Thread.currentThread().getName(), tl.get());
        
        new Thread(() -> {
            log.info("{}:value = {}", Thread.currentThread().getName(), tl.get());
            tl.set("Hello Thank You ~");
            log.info("{}:value = {}", Thread.currentThread().getName(), tl.get());
        }, "thread-1").start();
        
        TimeUnit.MILLISECONDS.sleep(500);
        log.info("{}:value = {}", Thread.currentThread().getName(), tl.get());
    }
}

运行打印结果如下:

23:28:20.801 [main] INFO com.ldx.test.threadlocal.ThreadLocalTest - main:value = Hello World ~
23:28:20.806 [thread-1] INFO com.ldx.test.threadlocal.ThreadLocalTest - thread-1:value = null
23:28:20.806 [thread-1] INFO com.ldx.test.threadlocal.ThreadLocalTest - thread-1:value = Hello Thank You ~
23:28:21.309 [main] INFO com.ldx.test.threadlocal.ThreadLocalTest - main:value = Hello World ~

小结

通过日志结果可以判断出:主线程与thread-1线程set 和 get 值都互不影响

3. 源码

在看源码之前我们先大概了解一下Thread,ThreadLocal,ThreadLocalMap,Entry都是什么关系?

3.1 ThreadLocal

ThreadLocal类其实很简单,核心方法就set,get,remove

withInitial:这个方法采用Lambda方式传入实现了 Supplier 函数接口的参数。

3.1.1 set()

哈? 超简单有没有。

public void set(T value) {
  // 获取当前线程(也就是当前是在哪个线程方法内执行当前set方法,获取的就是哪个线程)
  Thread t = Thread.currentThread();
  // 获取一个Map 后面讲此Map
  ThreadLocalMap map = getMap(t);
  if (map != null)
    // this:指的是当前的ThreadLocal实例对象
    map.set(this, value);
  else
    createMap(t, value);
}
  1. 获取当前线程
  2. 获取Map
  3. map不为空就set,为空就create

首先看下getMap方法:

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

昂~ 就是把线程对象threadLocals属性返回去了

等等 🤔,为什么是线程对象的threadLocals属性呢?

看到这里应该就有点明白了,原来ThreadLocal set值的时候操作的是Thread对象中的threadLocals,就说为什么线程间数据隔离呢,原来值都存储在了线程对象自己的成员变量中。

我们继续看下createMap(t, value)具体干了些什么

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

创建了一个ThreadLocalMap对象赋值给了线程的threadLocals属性

小结:

ThreadLocal::set 方法逻辑还是比较清晰的

  1. 获取当前线程
  2. 获取当前线程的threadLocals属性值作为操作的map
  3. map不为空就set,为空就create一个ThreadLocalMap对象赋值给threadLocals

这里先不纠结ThreadLocalMap是个什么东东,就当它是个map

3.1.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) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  // 如果map为null 就初始化一个value(其实就是null)
  return setInitialValue();
}
  1. 获取当前线程 & 返回当前线程的threadLocals作为map
  2. map不为空就根据this(当前的ThreadLocal实例对象)获取到Map的Entry对象
  3. 如果Entry不为null就返回

这么一看get方法也没啥

3.1.3 remove()

public void remove() {
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    m.remove(this);
}

这?就没啥好说的了

3.2 ThreadLocalMap

在看ThreadLocal方法时 反复提到了ThreadLocalMap对象 并且 对象方法的操作其实都是操作的该对象

Thread --> ThreadLocal.ThreadLocalMap threadLocals = null;),它是个什么呢?

ThreadLocalMap 其实是ThreadLocal的一个内部静态类:

3.2.1 field

//初始化容量大小
private static final int INITIAL_CAPACITY = 16;

//存放数据的Entry 也就是ThreadLocal set 的值真正存储到了Entry[]中
private Entry[] table;

//表中实际存储Entry的个数
private int size = 0;

//重新分配表大小的阈值,默认为0
private int threshold; // Default to 0

3.2.2 Constructor

ThreadLocalMap 构造方法源码如下:

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

创建ThreadLocalMap对象时其实内部是创建了一个Entry对象:new Entry(firstKey, firstValue)

  • firstKey:ThreadLocal对象
  • firstValue: ThreadLocal set的时候传入的值

重新整理一下链路:

  1. ThreadLocal set()

    • set 方法中首先获取到当前线程

    • 获取当前线程的 threadLocals

    • 首次进来 threadLocals值为null

  2. createMap()

    • new ThreadLocal(ThreadLocal,Object)
  3. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

    • new Entry(ThreadLocal<?> k, Object v)

3.2.3 getEntry()

private Entry getEntry(ThreadLocal<?> key) {
  // * 使用当前的ThreadLocal实例 计算出数组index
  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);
}

getEntry()时使用的是ThreadLocal实例作为key计算的数组index

3.2.4 set()

private void set(ThreadLocal<?> key, Object value) {
    //将 table 表赋给 tab
    Entry[] tab = table;
    //记录未设置值前 table 的长度
    int len = tab.length;
    // * 使用当前的ThreadLocal实例 计算出数组index
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
       //得到当前 entry 的 key(TreadLocal 的弱引用)
        ThreadLocal<?> k = e.get();
       //Threadloca1 对应的key存在,直接覆盖之前的值
        if (k == key) {
            e.value = value;
            return;
        }
        //key为nu11 ,但是值不为nu11,说明之前的ThreadLocal 对象已经被回收了
        if (k == null) {
            //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //ThreadLoca1对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    /**
     * cleanSomes1ots用于清除那些 e.get()==nu11 的元素,
     * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置nu11.
     * 如果没有清除任何 entry ,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
     * rehash(执行一次全表的扫描清理工作)
     */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

3.2.5 remove()

private void remove(ThreadLocal<?> key) {
    //将 table 表赋给 tab
    Entry[] tab = table;
    //记录未设置值前 table 的长度
    int len = tab.length;
    // * 使用当前的ThreadLocal实例 计算出数组index
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//线性探测寻找
        if (e.get() == key) {
            //清理失效的 key
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

3.3 Entry

Entry 其实 是 ThreadLocalMap 的内部静态类。好一个套娃~ ThreadLocal ---> ThreadLocalMap ---> Entry

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

  Entry(ThreadLocal<?> k, Object v) {
    //Entry 里的Key存放 ThreadLocal对象的是弱引用
    super(k);
    value = v;
  }
}

弱引用就是只要发生垃圾回收弱引用指向的对象就会被回收。

这里 Entry 对象继承了 WeakReference,通过super(k)可以了解到弱引用指向了ThreadLocal实例,大概关系是:

4. 内存分布

  1. 假设随着 Stack 的执行结束,ThreadLocal Ref 被GC回收了(假设使用的是线程池中的Thread对象)。

  2. ThreadLocalMap 中的 key只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

  3. 没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef-->currentThread-->threadLocalMap-->entry --> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏(常驻内存)。

原因
1.没有良好的编程思想,使用完未释放。

2.Thread对象 生命周期太长了。

参考:

https://blog.csdn.net/weixin_44075132/article/details/115543608

posted @ 2022-07-31 16:25  张铁牛  阅读(74)  评论(0编辑  收藏  举报