ThreadLocal的熟悉与使用

105 阅读21分钟

1.ThreadLocal介绍

ThreadLocal 是Java JDK中提供的一个类,用于提供线程内部的局部变量,这种变量在多线程下的环境下去访问时能保证各个线程的变量独立于其他线程的变量。也就是说,使用ThreadLocal 可以提供线程内部的局部变量(通过ThreadLocal的set() 和 get() 方法),不同的线程之间不会互相干扰,这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。听起来好像挺复杂的,下面我们使用一个简单的案例来解释一下ThreadLocal的作用。

案例说明: 演示的代码将会使用一个TestData类表示存放在线程里面的数据,然后开启10个线程,在每个线程中设置数据后紧接着获取数据,并且使用Thread.currentThread().getName()标识对应的线程。然后为每个线程设置名称,方便我们观察线程的数据情况。

在不使用ThreadLocal和加锁的情况下:

public class ThreadLocalDemo {
    public static void main(String[] args) {
        TestData testData = new TestData();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    testData.setData("数据XXX,当前线程是===>"
                            + Thread.currentThread().getName());
                    System.out.println("当前线程是: "
                            + Thread.currentThread().getName()
                            + ",存放的数据是:" + testData.getData());
                }
            });

            thread.setName("线程===>" + i);
            thread.start();
        }
    }

    static class TestData {
        private String data;
        
        public void setData(String data) {
            this.data = data;
        }

        public String getData() {
            return data;
        }
    }
}

运行上面的代码结果如下:

image.png 如上图所示:我们发现有的线程拿到的数据是其他线程的,也就是各个线程之间的数据错乱了,这种情况是一种错误,因为线程之间的数据发生了相互干扰的情况。比如上图中选中的部分,线程1存放的数据被线程4拿到了。正确的情况应该是,线程1存放的数据,应该也是由线程1取。即各个线程之间不应该相互干扰

解决上面线程间错误的问题有两种方法,一是加锁,二是使用ThreadLocal,接下来看加锁的方案,代码如下:

    public static void main(String[] args) {
        TestData testData = new TestData();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 加锁解决线程间数据错乱的问题
                    synchronized (ThreadLocalDemo.class){
                        testData.setData("数据XXX,当前线程是===>"
                                + Thread.currentThread().getName());
                        System.out.println("当前线程是: "
                                + Thread.currentThread().getName()
                                + ",存放的数据是:" + testData.getData());
                    }
                }
            });

            thread.setName("线程===>" + i);
            thread.start();
        }
    }

在每个线程的run方法中添加一个synchronized锁,在多个线程的情况下,限制每次只能有一个线程存取数据,这样就能解决线程间数据干扰的问题,运行结果如下:

image.png 如上图所示,使用加锁的方案后,线程存数据和取数据的线程都是同一个了,不会出现线程1的数据被线程2取到了 然后我们再看下使用ThreadLocal的方式解决线程间数据相互干扰的问题,代码如下:

    static class TestData {
        private ThreadLocal<String> tl = new ThreadLocal<>();

        public void setData(String data) {
           tl.set(data);
        }

        public String getData() {
            return tl.get();
        }
    }

运行结果如下:

image.png

如上图所示,存储数据时使用TreadLocal的set方法,取数据时使用ThreadLocal的get()方法,这样也能解决线程间数据相互干扰的问题,具体原理会在后面源码分析部分解析

看完上面的例子,可能会有小伙伴心中有疑问,既然加锁可以解决线程间数据相互干扰的问题,那么为啥还需要设计出一个ThreadLocal呢?其实这得联系synchronized和ThreadLocal的区别,synchronized是一种同步机制,采用以“时间换空间”的方式,只是提供一份数据,让不同的线程排队使用,它的侧重点在于多个线程之间同步访问资源。而ThreadLocal则是以“空间换时间”,为每一个线程都提供了一份数据的副本,从而实现同时访问而互不干扰,它侧重于多线程中让每个线程之间的数据的相互隔离。在上面的例子中我们强调的是线程数据隔离的问题,使用synchronized不仅消耗性能(加锁会使程序的性能降低),而且加锁更加使用于数据共享的场景,用在此处并不合适,使用TreadLocal可以使程序获得更高的并发性。

通过上面的例子,相信读者已经可以简单的理解ThreadLocal是啥以及它的作用了,接下来我们将从源码分析ThreadLocal,一点点解开其背后的神秘面纱。

2.ThreadLocal源码解析

2.1 常用方法

方法描述
ThreadLocal()构造方法,创建ThreadLocal对象
public void set(T value)设置当前线程绑定的数据
public T get()获取当前线程绑定的数据
public void remove()移除当前线程绑定的数据

2.2 结构设计

这里我们说的ThreadLocal都是JDK 1.8之后的,在JDK1.8中,每个Thread维护一个ThreadLocalMap哈希表,这个Hash表的Key是ThreadLocal本身,value是要存储的数据Object具体的过程如下:

1.每个Thread都有一个Map,名为ThreadLocalMap 2.ThreadLocalMap里面存储了ThreadLocal对象(Key)和线程的数据副本(Value) 3.Thread内部的ThreadLocalMap是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的数据 4.对于不同的线程,每次获取副本数据时,别的线程并不能获取当前线程的副本数据,实现了副本数据的隔离。

ThreadLocal的结构如下所示:


image.png

2.3 类图


image.png

ThreadLocalMap 是ThreadLocal的内部类,没有实现Map接口,而是使用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。并且继承自弱引用的接口

2.4 源码分析

下面先解释下ThreadLocal中会用到的存储结构,Entry类,代码如下所示:

    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

从Entry的代码中我们可以得知,Entry继承自WeakReference,并且使用ThreadLocal作为key.并且这个key只能是ThreadLocal对象.

补充:在ThreadLocal中会自定义一个ThreadLocalMap以Key、Value的方式保存值,类似于HashMap。我们都知道HashMap会有Hash冲突,而解决HashMap冲突的方法是链地址法,而ThreadLocalMap解决冲突的方法是线性探测法,该方法一次探测一个地址,直到有空地址可插入,若是整个空间都找不到空余的地址,则产生溢出。比如,假设当前数组的长度为16,如果计算出来的索引为14,而数组中位置为14的地方已经有值了,并且这个值的key和当前待插入数据的key不一样,那么此时就发生了哈希冲突。线性探测法就是说这时候可以通过一个线性的函数,将当前的位置作为输入,经过线性函数运算后得到一个输出,,比如我们确定这个线性函数为y = x + 1,y为计算后的索引值,x为输入的索引值,我们这时候可以将14输入线性函数,得到新的索引为15,取数组中位置为15的位置的值判断,如果还是冲突,则会溢出,这时候可以判断溢出的时候就从位置0继续使用线性探测法查找可以插入数据的位置。

2.4.1 set方法分析

当我们使用ThreadLocal的时候,首先会通过new关键字创建一个ThreadLocal对象,然后调用ThreadLocal的set方法保存我们想要保存的值,set方法执行过程的源码如下: 使用ThreadLocal对象调用set方法存储数据的时候,会首先调用下面的set方法,下面的方法会将当前线程对象和需要保存的数据一起传入内部的set(Thread,value)方法里。

 public void set(T value) {
 // 调用内部的set方法,并且将当前的线程对象和要保存的值传递过去
        set(Thread.currentThread(), value);
        if (TRACE_VTHREAD_LOCALS) {
            dumpStackIfVirtualThread();
        }
    }

set(Thread,value)方法会首先去获取下当前线程是否已经关联了ThreadLocalMap,如果已经关联了就直接取出这个Map,调用其set方法保存数据,否则创建一个新的ThreadLocalMap,并保存数据,并且将创建的ThreadLocalMap赋值给当前线程里面的threadLocals变量。

  private void set(Thread t, T value) {
       // 获取和当前线程相关联的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 如果获取到的ThreadLocalMap不为空,则直接调用ThreadLocalMap的set方法直接赋值
        // 否则使用当前线程和需要保存的值直接创建ThreadLocalMap对象,需要注意的是,这里不用再
        // 调用ThreadLocalMap的set方法了,因为值的保存操作会在ThreadLocalMap的构造函数中完成
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    
    // 通过线程去拿与其关联的ThreadLocalMap,从下面的代码
    // 可以看出,ThreadLocalMap被作为了一个成员变量声明到了线程
    // Thread类中
  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // 使用线程和需要保存的数据创建一个ThreadLocalMap对象,
    // 并将其赋值给当前线程的threadLocals变量
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

当线程中关联的ThreadLocalMap为空时会使用ThreadLocal作为key,需要保存的数据作为Value去新建一个ThreadLocalMap对象,下面是ThreadLocalMap的构造方法。

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // table 和HashMap中的table类似,这里的INITIAL_CAPACITY是16,必须为2的幂次方,
        // 原因后面会介绍
        // 这里主要是初始化table数组,数据的元素类型是Entry的,初始容量是16
            table = new Entry[INITIAL_CAPACITY];
            // 和HashMap一样,使用key的HashCode和长度减一做与操作,计算出一个索引值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
             // 将要保存的数据包装成Entry后保存到索引对应的位置
            table[i] = new Entry(firstKey, firstValue);
            // 记录当前ThreadLocalMap的大小
            size = 1;
            // 设置扩容的阈值
            setThreshold(INITIAL_CAPACITY);
        }
        
        // 设置阈值,当阈值达到设置长度的2/3时进行扩容操作
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

如果线程中的ThreadLocalMap不为空的情况下,会被取出来调用其set(ThreadLocal<?> key, Object value) 方法保存数据,这个方法的具体解析如下面代码中的注释所示。

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            // 记录下table的长度
            int len = tab.length;
            // 计算待插入元素在table数组中的索引,使用的是和HashMap类似的,使用key的hash值和
            // 数组的长度减一做与操作。这里的数组长度需要是2的幂次方
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 如果待插入的key已经存在,则直接使用待插入的数据覆盖原来的数据即可
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 如果key为null,但是数据value不为null,则说明之前的ThreadLocal对象已经被回收了
                if (k == null) {
                // 使用新的元素替换之前的元素
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // ThreadLocal对应的key不存在并且没有找到旧的元素,则在空元素的位置新建一个Entry
            tab[i] = new Entry(key, value);
            // 增加ThreadLocalMap的size
            int sz = ++size;
            // cleanSomeSlots用于清除e.get == null的元素
            // 因为这种数据key关联的对象已经被回收,所以Entry(table[index])可以被置为null,
            // 如果没有清除任何的Entry,并且当前的使用量达到了负载因子所定义的(长度的2/3),
            // 那么进行再次哈希计算的逻辑(rehash),执行一次全表的扫描清理工作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        // 循环获取数组的下一个索引
         private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

在上面的代码中我们提到了数组的长度需要是2的幂次方,原因如下: 我们都知道通常情况下如果想要将某个Hash值映射到数组的索引上,通常会使用到取模(%)运算符,因为这可以确保生成的索引在数组的范围类,例如生成数组长度为16,计算的结果会在0到15之间。,那我们在HashMap和ThreadLocalMap中要规定数组的长度必须是2的幂次方呢?那是因为计算数组的索引没有采用传统的取模(%)运算,而使用的是与(&)操作,如下图所示: 在这里插入图片描述 使用与操作会比取模操作快很多,但是只有当数组长度为2的幂次方时,hashcode&(数组长度 - 1) 才等价于 hashcode % 数组长度,,其次保证数组长度为2的幂次方也恶意减少冲突的次数,提高查询的效率。例如,若数组长度为2的幂次方,则数组长度减一转为二进制必定是1111....的形式,在和hashcode二进制做与操作时效率会非常高,而且空间不浪费。举个反例,假设数组的长度不是2的幂次方,不妨设为15,则数组的长度减一为14,对应的二进制为1110,在与hashcode做“与操作”时,由于最后一位都是0,这就会导致数组位置索引最后一位为“1”的位置(如0001,0101,1011,1101)永远无法存放元素,浪费空间,并且导致数组可使用的位置比数组长度小很多,发生哈希冲突的几率增大,并且降低了查询效率。 注意:这里的hashcode不是指通过对象的hashCode()方法获取到的值,而是经过一些算法得到的一个哈希值

set方法代码的执行流程

  1. 根据key的hashcode计算出索引"i",然后查找到"i"位置上的Entry
  2. 若Entry存在,并且key等于传入的key,那么直接给找到的Entry赋新的value值
  3. 若Entry存在,但是key为null,则调用replaceStaleEntry()方法更换key为空的Entry
  4. 若不存在上面的情况,则开启循环检测,直到遇到为null的位置,在这个null位置新建一个Entry,然后插入,同时将ThreadLocalMap的size增加1
  5. 调用cleanSomeSlots方法,清理Key为null的Entry,最后返回是否清理了Entry的结果,然后再判断ThreadLocalMap的size是否大于等于扩容的阈值,如果达到了,需要执行rehash函数进行全表扫描清理,清理完ThreadLocalMap的size还是大于阈值的3/4的化,那么就需要进行扩容。扩容操作会将数组的长度扩容为之前的两倍

2.4.2 get方法分析

get方法是获取当前线程中保存的值,调用的方式就是使用ThreadLocal的对象调用get方法,ThreadLocal中的get方法如下所示:

   public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 根据当前线程拿到ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
         // ThreadLocalMap不为空的情况下,调用ThreadLocalMap的getEntry方法,
         // 传入当前的ThreadLocal(key),拿到当前ThreadLocal对应的数据并返回给调用者
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 将数据做一个类型转换,然后返回
                T result = (T)e.value;
                return result;
            }
        }
        // ThreadMap为空的情况下,会调用setInitialValue方法返回一个值
        return setInitialValue();
    }
    // 拿到线程对应的ThreadLocalMap对象
   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // 设置ThreadLocal的初始值
    private T setInitialValue() {
        // 通过initialValue方法获取初始值,initialValue是一个可供
        // 子类重写的方法,子类可以重写initialValue方法提供一个默认
        // 值,不重写的情况下为null.
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 根据当前线程拿到ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 如果ThreadLocalMap不为空,则将通过initialValue获取的值设
        // 置给ThreadLocalMap
        if (map != null) {
            map.set(this, value);
        } else {
        // 如果通过当前线程没有获取到ThreadLocalMap,则创建一个ThreadLocalMap并将通过
        // initialValue方法获取到的值设置给它
            createMap(t, value);
        }
        // 不是重点,不分析,这里是为了解决ThreadLocal引用泄漏的问题的,TerminatingThreadLocal 
        // 提供了一种机制,可以在线程终止时自动清理其绑定的数据。
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        
        return value;
    }

// ThreadLocalMap中的getEntry方法,参数:key
      private Entry getEntry(ThreadLocal<?> key) {
            // 通过key的threadLocalHashCode计算出数组table中的索引位置
            int i = key.threadLocalHashCode & (table.length - 1);
            // 通过计算出的索引值拿到对应的Entry元素
            Entry e = table[i];
            // 若拿到的元素不为null,并且元素的key和当前传入的key相同,则证明找到了
            // 传入的key对应的Entry元素,直接返回
            if (e != null && e.get() == key)
                return e;
            else
            // 否则可能是在插入数据时有冲突被放到了其他位置了,通过getEntryAfterMiss方法
            // 继续查找其他位置
                return getEntryAfterMiss(key, i, e);
        }

        // 查找Entry元素,参数key:待查找元素的key,i: 当前元素的索引,索引i对应的元素Entry
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            // 拷贝一份当前的table数组
            Entry[] tab = table;
            // 记录当前数组的长度
            int len = tab.length;
            // 若是元素Entry不为null,就循环查找,直到找到待查找元素key对应的Entry为止
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 如果e的key等于待查找的元素的key,证明找到了直接返回就行
                if (k == key)
                    return e;
                // 如果e的key为null,则调用expungeStaleEntry方法替换
                if (k == null)
                   // 清除key为null的Entry
                    expungeStaleEntry(i);
                else
                   // 通过线性探测法继续寻找下一个位置,插入值的时候如果有冲突也是通过这个方法
                   // 解决的,所以查询值的时候,如果不在通过key的hashcode值计算出的索引位置
                   // 就可以通过这个函数继续寻找下一个位置。直到找到待查找key对应的数据为止
                    i = nextIndex(i, len);
                e = tab[i];
            }
            // 如果没有找到就返回null
            return null;
        }

2.4.3 remove方法分析

ThreadLocal的remove方法用于删除当前线程中保存的ThreadLocal对应的Entry,代码如下所示:

 public void remove() {
         // 首先通过当前线程获取到TheadLocalMap,不为null的情况下
         // 删除当前ThreadLocal保存的Entry;如果为null,表示
         // 不需要删除
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             // 调用ThreadLocalMap的remove方法删除保存的Entry
             m.remove(this);
         }
     }
 
 // ThreadLocalMap中删除ThreadLocal对应的Entry
 private void remove(ThreadLocal<?> key) {
    // 拷贝一份table数组
    Entry[] tab = table;
    // 记录数组的长度
    int len = tab.length;
    // 根据当前的key计算出Entry的索引位置"i"
    int i = key.threadLocalHashCode & (len-1);
    // 在数组中遍历查找key对应的entry
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
         // 查找到key对应的entry
        if (e.get() == key) {
            // 调用Entry的clear方法清理掉Entry,其实就是将当前的Entry引用置为null
            // 等待垃圾回收器回收
            e.clear();
            // 清除key为null的Entry
            expungeStaleEntry(i);
            return;
        }
    }
 
 // Entry中的clear方法
 public void clear() {
        this.referent = null;
    }

3.ThreadLocal内存泄漏分析

3.1 相关概念

3.1.1 内存溢出

内存溢出(Memory overflow)是指没有足够的内存提供给申请者使用

3.1.2 内存泄漏

内存泄漏(Memory leak)指的时程序中已经动态分配的堆内存由于某种原因未释放或者是无法释放,造成系统内存的浪费,从而导致程序运行速度减慢升值系统崩溃的严重后果,内存泄漏的堆积最终将会导致内存溢出

3.1.3 强引用

强引用(Strong Reference)就是我们常见的普通对象的引用,比如:Object strongRef = new Object();就是一种强引用,只要某个对象有强引用指向它,垃圾回收器(Garbage Collector) 就不会回收该对象。

3.1.4 弱引用

弱引用(Weak Reference) 是一种特殊的引用类型,用于改善内存管理。弱引用允许垃圾回收器回收被其引用的对象,即使该对象仍然有活动的弱引用存在。它通常用于缓存、引用监听器和防止内存泄漏的场景。

3.2 内存泄漏是否和key使用的弱引用有关

有读者可能会猜测ThreadLocal的内存泄漏可能会和Entry中使用了弱引用的key有关系,其实这个猜测不太准确,下面就从两个方面分析下ThreadLocal内存泄漏的原因

3.2.1 假设key使用强引用

假设ThreadLocalMap中的key使用了强引用,,则此时ThreadLocal的内存图如下所示: 在这里插入图片描述

如上图所示,假设在业务代码中使用玩ThreadLocal后,ThreadLocal引用被回收了,但是因为ThreadLocalMap的Entry强引用ThreadLocal,会造成ThreadLocal无法被回收,这时在没有手动删除Entry和CurrentThread的情况下,始终会有强引用链:CurrentThread引用===>CurrentThread===> ThreadLocalMap===>Entry.最终导致Entry无法被回收导致内存泄漏,所以ThreadLocalMap中的key使用了强引用是无法完全避免内存泄漏的

3.2.2 假设key使用弱引用

假设key使用了弱引用,ThreadLocal的内存图如下所示 在这里插入图片描述

假设业务代码中使用完ThreadLocal后,然后ThreadLocal被回收了,此时由于ThreadLocalMap只持有ThreadLcoal的弱引用,并且没有任何的强引用指向ThreadLocal实例,所以ThreadLocal实例可以顺利的被垃圾回收器回收,此时就会导致Entry的key为null,这时候如果我们没有手动删除这个Entry以及CurrentThread仍然运行的前提下,也存在强引用链:CurrentThread引用===>CurrentThread===> ThreadLocalMap===>Entry===>Value,而这里的Value不会被回收,但是这块Value永远不会被访问到了,因为key已经被回收了,导致Value内存泄漏,所以ThreadLocalMap中的key使用了弱引用,也有可能导致内存泄漏。

3.2.3 内存泄漏的真实原因

通过上面的两种对key使用强引用和弱引用的方式分析,我们发现ThreadLocal的内存泄漏和ThreadLocalMap的key是否使用弱引用是没有关系的,真正引起内存泄漏的原因主要有两点,第一点是当ThreadLocal被回收后,没有手动删除ThreadLocalMap的Entry,这时只要我们使用完后调用ThreadLocal的remove方法删除对应的Entry就可以避免内存泄漏。第二点是当ThreadLocal被回收后,CurrentThread依然在运行。由于ThreadLocalMap是Thread的一个属性,被当前线程引用,所以它的生命周期和Thread一样长,那么在使用完ThradLocal,如果当前的线程也一起随之结束,那么ThreadLocalMap就可以被垃圾回收器回收,从根源上避免了内存泄漏

结合上面的分析可以知道,ThreadLocal内存泄漏的真实原因是,由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的Entry就会导致内存泄漏。

3.2.4 ThreadLocalMap的key使用弱引用的原因

有的读者可能会问,既然ThreadLocalMap的key使用强引用和弱引用都无法避免内存泄漏,那么为啥偏偏选择使用弱引用呢?经过前面的分析我们发现ThreadLocalMap的key无论使用强引用还是弱引用都无法完全避免内存泄漏,如果想要避免内存泄漏,主要有两种方式:

  1. 使用完ThreadLocal后,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal后,当前线程也随之结束

第一种方式相对简单,直接调用ThreadLocal提供的remove方法就行,但是第二种方式就不是那么好控制了,因为如果在使用线程池的场景,第二种方式就会出问题,因为线程池中的线程有复用的情况。那么回到问题,为啥ThreadLocalMap的key偏偏要使用弱引用。其实在ThreadLocal中的get/set/getEntry方法中会对key为null的情况进行判断,如果key为null,则value也会被置为null,这时候使用弱引用就会多一层保障,假设在ThreadLocal,CurrentThread依然运行的情况下,如果忘记调用了ThreadLocal的remove方法,ThreadLocalMap的key由于是弱引用,所以可以被回收,这时候key就为null,然后在下一次ThreadLocalMap调用set、get、getEntry中的任何一个方法都会将key为null的Entry清除掉,从而避免了内存泄漏,这就是为什么ThreadLocalMap的key要使用弱引用的原因。

4.ThreadLocal使用场景

ThreadLocal目前使用最常见的就是Android中Handler机制中的Looper,

    @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    // 创建Looper时需要调用Looper的prepare方法
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

我们都知道Android的Handler机制中,每个线程有一个Looper,并且每个线程的Looper是互相不干扰的,要实现这种功能就得借助ThreadLocal,创建Looper的时候先判断当前创建Looper的线程是否已经有了Looper,没有再创建,有的化就直接使用。

另外还有一种情景就是在服务端开发的时候,读取MySQL的数据库连接对象,如果是多个线程去读取的情况下,每个线程都需要维护一个自己的数据库连接,这样使用完后就释放自己的连接,不会影响到其他线程的数据连接。