一、常见的两种方法

set(T value)

  1. public class ThreadLocal<T> {
  2. public void set(T value) {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. }
  10. ThreadLocalMap getMap(Thread t) {
  11. return t.threadLocals;
  12. }
  13. }
  14. public class Thread implements Runnable {
  15. /* ThreadLocal values pertaining to this thread. This map is maintained
  16. * by the ThreadLocal class. */
  17. ThreadLocal.ThreadLocalMap threadLocals = null;
  18. }

可以看出在set方法中,首先是与获取当前线程相关的map数据结构(Thread类中肯定有这个成员变量的声明),类型却是ThreadLocal.ThreadLocalMap,其实查看源码可以知道,ThreadLocal.ThreadLocalMap的结构同HashMap结构类似。如下:

  1. public class ThreadLocal<T> {
  2. static class ThreadLocalMap {
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. /** The value associated with this ThreadLocal. */
  5. Object value;
  6. Entry(ThreadLocal<?> k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }
  11. private static final int INITIAL_CAPACITY = 16;
  12. private Entry[] table;
  13. private int threshold; // Default to 0
  14. }
  15. }

可以看到,ThreadLocal.set(T value)会将value对象绑定到当前线程内部的ThreadLocal.ThreadLocalMap对象上,键名是ThreadLocal对象,键值是value对象。
1 这么来说,各个线程在进行set操作时,将目标变量绑定到线程自身的ThreadLocal.ThreadLocalMap对象上,线程之间是相互隔离,互不影响的,这一点其实跟线程安全并没有关系,线程安全是发生在多个线程同时访问某一共享变量时,出现线程之间相互影响导致出现错误结果的现象,而使用ThreadLocal访问的是线程自身内部的变量,所以说不会出现线程安全问题。 ThreadLocal - 图12 从ThreadLocal的set方法可以看出,它是将当前声明的共用变量作为key,也就是说在线程内部,只能维护一个与此ThreadLocal相关的Object对象,如果你想在当前线程绑定多个对象,那你就得声明多个ThreadLocal变量了。不过灵活的用法是绑定的数据结构是HashMap,而不是简简单单的Object。

  1. private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
  2. private static final ThreadLocal<HashMap<String, Object>> THREAD_LOCAL = new ThreadLocal<>();

2 get()

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

get方法首先同set方法一样,获取当前线程的内部变量threadLocals(ThreadLocal.ThreadLocalMap类型),上面说过,它是一种类似HashMap的数据结构,可以根据key快速取出map结构中对应的value,不过这里的key就是ThreadLocal本身。如果threadLocals中没有此key,则返回初始化设置的默认值。

二、弱引用

我们知道Java中对象的引用分为四种,强引用,软引用,弱引用,虚引用,它们的引用强度依次降低。
JVM在回收垃圾对象时,是不对强引用对象进行回收的。那么软引用和弱饮用的区别在哪里?如果可用内存还没有降低到一个阈值临界点,JVM在扫描垃圾对象时,只会回收持有弱引用的对象,而不会回收持有软引用的对象。只有可用内存降低到阈值临界点时,JVM才会回收持有软引用的对象,所以说JVM在回收垃圾时,不管可用内存有没有降低到阈值临界点,都会回收持有弱引用的对象。

  1. public class ThreadLocal<T> {
  2. static class ThreadLocalMap {
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. /** The value associated with this ThreadLocal. */
  5. Object value;
  6. Entry(ThreadLocal<?> k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }
  11. private static final int INITIAL_CAPACITY = 16;
  12. private Entry[] table;
  13. private int threshold; // Default to 0
  14. }
  15. }

从程序中可以看出ThreadLocalMap的Entry的key是持有ThreadLocal对象的弱引用的,假设某一时刻,JVM进行垃圾回收,那么这些Entry中的Key对象全部被清除,Entry中只剩下Value对象,但是却无法通过有效的Key来取出这些Value对象。由于Entry对象是强引用,所以这些Value对象会一直堆积在内存中,虽然ThreadLocal的set和get方法做了一些额外的处理,但是还是有内存泄露的风险。
另外,将ThreadLocal声明为static,是有一定道理的,我们知道类中的静态变量是有可能成为GC Root的,这样ThreadLocal是强引用,ThreadLocalMap中的Entry肯定也是强应用了,因为可以通过GC Root找到,所以Entry中的Key不会被JVM回收,那么ThreadLocal在执行remove的时候,可以正确定位到Entry并删除。正确的用法如下:

  1. public void method1(String arg) {
  2. LOGGER.info("invoke public method1.");
  3. try {
  4. THREAD_LOCAL.set(arg);
  5. method2();
  6. } finally {
  7. THREAD_LOCAL.remove();
  8. }
  9. }

这一点,阿里巴巴集团开发手册规约(第14页第15条)上也明确提出:

  1. 【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static
    修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
    此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只
    要是这个线程内定义的)都可以操控这个变量。

参考:
1 手撕面试题ThreadLocal,http://www.jiangxinlingdu.com/interview/2019/06/19/threadlocal.html。