侧边栏壁纸
博主头像
Ysfun博主等级

一名热爱技术、喜欢折腾的小小程序猿

  • 累计撰写 42 篇文章
  • 累计创建 14 个标签
  • 累计收到 25 条评论

目 录CONTENT

文章目录

源码剖析:ThreadLocal

Ysfun
2022-07-26 / 1 评论 / 3 点赞 / 253 阅读 / 2,236 字

ThreadLocal用来提供线程级别变量,变量只对当前线程可见。相比于使用锁控制共享变量访问顺序的解决方案,ThreadLocal通过空间换时间的策略,每个线程都有属于自己的线程私有变量,很好地规避了线程竞争的问题。

首先回答两个问题:

  1. 什么是ThreadLocal

ThreadLocal顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程对这个ThreadLocal中数据的读写是线程隔离的,互相不影响的,它提供了一种将可变数据通过每个线程持有一份私有的独立副本从而实现线程隔离的机制。

  1. ThreadLocal的大致实现思路?

Thread类中有一个ThreadLocal.ThreadLocalMap类型的属性threadLocals,也就是说每个线程拥有一个自己的ThreadLocalMapThreadLocalMap是一种针对ThreadLocal定制化的HashMap,它的KeyThreadLocal对象(实际是ThreadLocal的弱引用),Value为储存的线程私有数据。每个线程往ThreadLocal中添加值时,都会向线程专属的ThreadLocalMap里存,读数据也是以某个ThreadLocal作为引用,在自己的Map中寻找对应的Key,从而实现线程隔离。

下面从源码出发进行分析。

1. 源码剖析

1.1 Thread类

Thread类中包含threadLocalsinheritableThreadLocals两个属性,它们都是ThreadLocal.ThreadLocalMap类型。可以发现这两个属性默认初始化为null,只有当调用ThreadLocalsetget方法时才会创建实例对象。

1.2 ThreadLocalMap类

ThreadLocalMapThreadLocal的内部类,是定制化的HashMap,其也是使用Entry封装K-V存储数据的,不同的是ThreadLocalMapEntryKey只能是ThreadLocal类型,并且是一个「弱引用」(Java中的引用介绍过Java中的四种引用)。

1.3 ThreadLocal类

方法:

常用API有get()set(T)remove()

set

源码:

public void set(T value) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 拿到当前线程的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null)
    // Key 为当前ThreadLocal对象
    map.set(this, value);
  else
    createMap(t, value);
}

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

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

可以发现,ThreadLocal中的set(v)方法实际上调用了ThreadLocalMap中的set(this, v)方法,EntryKey为当前ThreadLocal对象。

get

源码:

public T get() {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 拿到当前线程的ThreadLocalMap
  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;
    }
  }
  // 当前线程的ThreadLocalMap为空,进行初始化
  return setInitialValue();
}

private T setInitialValue() {
  // 初始默认value为null
  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;
}

ThreadLocal类的get()方法的执行流程为:1. 拿到当前线程ThreadLocalMap类型的的私有属性threadLocals;2. 以当前ThreadLocal对象作为threadLocalsMapKey,拿到并范围对应的Value

remove

源码:

public void remove() {
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    // 删除当前线程ThreadLocalMap中key为当前ThreadLocal对象的Entry
    m.remove(this);
}

ThreadLocal类中的remove()方法是删除当前线程中threadLocals属性Key为当前ThreadLocal对象的Entry

小结

每个线程内部都有一个名为threadLocals类型为ThreadLocalMap的成员变量,其中key为我们定义的ThreadLocal变量的this引用,value则为我们执行set方法设置的值。

  • 第一次操作线程的ThreadLocalMap属性时,会初始化一个ThreadLocal.ThreadLocalMap对象,set(v)会存入以参数为valueK/V数据;get()会存入以nullvalueK/V数据。
  • ThreadLocalMap存值操作的入口为ThreadLocal.set(v)方法,并以当前ThreadLocal对象为key,参数vvalue
  • ThreadLocalMap取值操作的入口为ThreadLocal.get()方法,key为当前ThreadLocal对象。

2. 代码实践

代码:

public class ThreadLocalTest {
    static ThreadLocal<Integer> countThreadLocal = new ThreadLocal<>();
    static ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            countThreadLocal.set(1);
            nameThreadLocal.set("alise");
            print("======thread-1  ThreadLocal赋值后:");
        }, "thread-1").start();

        new Thread(() -> {
            print("======thread-2  ThreadLocal赋值前:");
            countThreadLocal.set(2);
            nameThreadLocal.set("bob");
            print("======thread-2  ThreadLocal赋值后:");
        }, "thread-2").start();
    }

    public static void print(String str) {
        System.out.println(str + "\n" + Thread.currentThread().getName() + ": \tcount: " + countThreadLocal.get() + "\tname: " + nameThreadLocal.get());
    }

}

运行结果:

说明:

  1. 定义了两个ThreadLocal对象:countThreadLocalnameThreadLocal
  2. 线程一先执行,调用ThreadLocal.set(v)方法,此时为初次操作ThreadLocal,因此会给线程一的成员变量threadLocals进行初始化并给ThreadLocalMap对象中插入两个Entry<K,V><countThreadLocal, 1><nameThreadLocal, alise>;注意此时只对线程一的threadLocals进行初始化和赋值操作,线程二的threadLocals仍为null
  3. 线程二在给ThreadLocal赋值前执行ThreadLocal.get()方法,此时会对线程二的成员变量threadLocals进行初始化,并插入两对默认Entry值:<countThreadLocal, null><nameThreadLocal, null>
  4. 线程二调用ThreadLocal.set(v)方法,此时threadLocals中已经有了两个Entry,调用ThreadLocalMap.set(this, v)方法后,线程二threadLocals中的两个Entry变为:<countThreadLocal, 2><nameThreadLocal, bob>,调用ThreadLocal中的get()方法会执行ThreadLocalMap.get(this),其中this为当前ThreadLocal对象

从上面的过程我们可以发现,两个线程都有自己私有的threadLocals对象,在进行ThreadLocal操作时两个线程的数据互相隔离、互不干扰,从而有效解决了线程同步问题。

3. ThreadLocal问题

3.1 ThreadLocal内存泄漏问题

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它的话,那么在系统GC的时候,这个ThreadLocal会被回收,这样一来,ThreadLocalMap中会出现keynullEntry,就无法访问这些keynullEntryvalue,如果当前线程迟迟不结束的话,那些keynullEntryvalue就会存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value,那这些value将一直都无法被回收,就会造成「内存泄漏」。

其实,在ThreadLocalMap的设计中已经考虑到了这种情况,并加上了一些防护措施:在ThreadLocalget()set()remove()的时候会清除ThreadLocalMapkeynull的所有Entry。为了代码更加规范,建议我们使用完ThreadLocal后最好手动调用remove()

3.2 ThreadLocalMap的key为何采用「弱引用」?

当存在线程复用的场景(如线程池),一个线程的寿命很长,大对象长期不被回收会影响系统运行效率与安全。下面举例说明:

举例:三个线程中的每个线程的ThreadLocalMap的其中一个Entry中的key使用的是同一个ThreadLocal类型变量的地址,都指向了ThreadLocal1

此时假设是强引用:多个线程依赖同一个`ThreadLocal1`,此时线程1的`ThreadLocal`使用结束,想要释放其内存,但由于强引用(因为还有其他线程指向`ThreadLocal1`),这就导致线程1持有的`ThreadLocalMap`中`key`为`ThreadLocal1`的`Entry`所占有的内存无法释放;如果采用弱引用的话,就仍可将其释放。

我们知道,ThreadLocalMap的生命周期基本和Thread的生命周期一样,当前线程如果没有终止,那么ThreadLocalMap始终不会被GC回收。对比使用强引用,弱引用可以保证不会因为大量key的积累而导致OOM,而对应的value可以通过get()set()remove()在下一次调用时清除。可见,内存泄漏的根源不是「弱引用」,而是ThreadLocalMap的生命周期和Thread一样长,造成内存积累。

3.3 ThreadLocal无法给子线程共享父线程的线程副本数据

异步场景下无法给子线程共享父线程的线程副本数据,可以通过 InheritableThreadLocal 类解决这个问题。

它的原理就是子线程是通过在父线程中调用 new Thread() 创建的,在 Thread 的构造方法中调用了 Thread的init 方法,在 init 方法中父线程数据会复制到子线程(ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);)。

但是我们做异步处理都是使用线程池,线程池会复用线程会导致问题出现。遇到这种情况我们需要自己解决。

3

评论区