在多线程环境下, 如果想要保证每个线程都能独立于其它线程独自运行, 可以使用 ThreadLocal 来解决; ThreadLocal 就是用于提供线程局部变量的一个工具, 也就是说 ThreadLocal 可以为每个线程创建一个单独的变量副本; 其概念与同步机制正好相反, 同步机制是保证多线程环境下数据的一致性; 而 ThreadLocal 则是保证多线程环境下数据的独立性.
本文将以代码的形式展示 ThreadLocal 的简单使用方式以及一些内部方法的原理.
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("我是线程 1");
System.out.println(threadLocal.get());
try {
// 测试如果移除了线程 2 后, 线程 1 是否还能够打印
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("我是线程 2");
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println("线程 2 移除了");
}
});
thread1.start();
thread2.start();
}
}
运行结果:
代码解读:
此代码就展示了两个线程环境下独立运行情况, 加入一个 5 s 时间的延迟是为了查看移除线程 2 后, 线程 1 是否还能够正常打印数据; 根据运行结果可以看得出两个线程互不影响.
那么 ThreadLocal 是怎样保证两个线程中的数据都是独立的呢 ¿ ?
要想知道 ThreadLocal 的实现原理, 首先要知道两个方法是如何实现的: set 方法, get 方法及 remove 方法.
remove 方法比较简单, 也是先获取到当前线程的 ThreadLocalMap, 然后删除就可以了.
ThreadLocal 中当前线程的 ThreadLocalMap 为空时会使用 ThreadLocalMap 的构造方法去新建一个 ThreadLocalMap, 如下:
通过源码可以看到, 构造的时候会新建一个 Entry 类型的数组, 并将第一次需要保存的键值存储到一个数组中, 完成一些初始化操作.
ThreadLocalMap 内部维护了一个哈希表来存储数据, 并且定义了加载因子等, 如下所示:
取值操作是直接获取到 Entry 对象, 使用 getEntry 方法, 如下:
在 ThreadLocal 的 get / set / remove 方法中, 都有清楚无效的 Entry 的操作, 这样做的目的就是为了降低内存泄露发生的可能.
导致内存泄露的原因:
假设 Entry 中的 key 没有使用弱引用 (弱引用就是无论空间是否充足, 都可以进行回收, 当然强引用使我们普遍使用的引用)的方式, 由于 ThreadLocalMap 的生命周期和当前线程一样长, 那么当引用 ThreadLocal 的对象被回收后, 由于 ThreadLocalMap 还持有 ThreadLocal 和对应的 value 的强引用, ThreadLocal 和对应的 value 是不会被回收的, 这就导致了内存泄露;
所以 Entry 以弱引用的方式避免了 ThreadLocal 没有被回收而导致的内存泄露, 但是此时的 value 仍然是无法回收的, 依然会导致内存泄露.
但是, ThreadLocalMap 已经考虑到了这种情况的存在, 因此在调用 get / set / remove 方法时会清除掉当前线程 ThreadLocalMap 中所有的 key 为 null 的 value; 这样就降低了内存泄露发生的概率; 所以我们在使用 ThreadLocal 的时候, 每次用完 ThreadLocal 都会调用 remove() 方法, 清除数据, 防止内存泄露.