ThreadLocal

ThreadLocal 不是用来解决共享变量问题的,它与多线程的并发问题没有任何关系。

1.简介

  早在 JDK 1.2 的版本中就提供Java.lang.ThreadLocal,1.5 开始,ThreadLocal 开始支持泛型。ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
  从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

2.用法

  • ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
  • ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
  • ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
  • ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

3.原理

3.1 线程共享变量缓存

Thread.ThreadLocalMap<ThreadLocal, Object>

  • Thread: 当前线程,可以通过Thread.currentThread()获取。
  • ThreadLocal:我们的 static ThreadLocal变量。
  • Object: 当前线程共享变量。

我们调用 ThreadLocal.get 方法时,实际上是从当前线程中获取 ThreadLocalMap<ThreadLocal, Object>,然后根据当前 ThreadLocal 获取当前线程共享变量 Object
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。

3.2 这种存储结构的好处

  1. 线程死去的时候,线程共享变量 ThreadLocalMap 则销毁。
  2. ThreadLocalMap 键值对数量为 ThreadLocal 的数量,一般来说 ThreadLocal数量很少,相比在ThreadLocal中用 Map 键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。

3.3 ThreadLocalMap 大致实现

ThreadLocalMapThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能(采用线性探测的方式解决Hash冲突,效率较低),其内部的Entry也独立实现Entry继承自WeakReference

Entry中的 key 只能是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继承自WeakReference弱引用,生命周期只能存活到下次 GC 前),但只有Key是弱引用类型的(注意看 3.1 中的虚线),Value并非弱引用。

ThreadLocalMap的成员变量:

static class ThreadLocalMap {  
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
}

3.4 ThreadLocalMap 的 Hash 冲突怎么解决

HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有 next 引用,也就是说ThreadLocalMap中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经有其他 key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap解决 Hash 冲突的方式就是简单的步长加 1 或减 1,寻找下一个相邻的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {  
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {  
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以建议:每个线程只存一个ThreadLocal变量,这样的话所有的线程存放到 map 中的 Key 都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入 Map 中时会极大的增加Hash冲突的可能。

3.5 ThreadLocalMap 弱引用问题

Entry继承自WeakReference(3.3 讲述过),这就导致了一个问题:ThreadLocal在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建ThreadLocal的线程一直持续运行,但是 ThreadLocal 已经被回收,那么这个 Entry 对象中的 value 就有可能一直得不到回收(线程中存在ThreadLocalMap<null, Object>的键值对),发生内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。

JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存回收。

但这样也并不能保证ThreadLocal不会发生内存泄漏,例如:

  • 使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。
  • 虽然ThreadLocalget,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用。

所以为了防止此类情况的出现,我们有两种手段:

  • 1.使用完线程共享变量后,显式调用ThreadLocalMap.remove方法清除线程共享变量;
  • 2.JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了(我并不确定)。

3.6 为什么使用弱引用?

从表面上看,发生内存泄漏,是因为 Key 使用了弱引用类型。但其实是因为整个 Entry 的 key 为 null 后,没有主动清除 value 导致。很多文章大多分析 ThreadLocal 使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?

官方说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

分析一下:

  • 假如key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
  • 假如key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 的 value 就会导致内存泄漏,而不是因为弱引用

在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

4.使用示例

public class Run {  
    private static ThreadLocal tl = new ThreadLocal();

    public static void main(String[] args) {
        if (tl.get() == null) {
            System.out.println("从未放过值");
            tl.set("我的值");
        }
        System.out.println(tl.get());
        System.out.println(tl.get());
    }

}

可参考:
ThreadLocal-面试必问深度解析
ThreadLocal内存泄漏真因探究