一、ThreadLocal 适合用在哪些实际生产的场景中
-
保存每个线程独享的对象
- 为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
- 这种场景通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。
-
案例
- 假设有个需求,即 2 个线程都要用到 SimpleDateFormat。代码如下所示:
/** 有两个线程,那么就有两个 SimpleDateFormat 对象 它们之间互不干扰,这段代码是可以正常运转的 运行结果是: 00:01 00:02 */ public class ThreadLocalDemo01 { public static void main(String[] args) throws InterruptedException { new Thread(() -> { String date = new ThreadLocalDemo01().date(1); System.out.println(date); }).start(); Thread.sleep(100); new Thread(() -> { String date = new ThreadLocalDemo01().date(2); System.out.println(date); }).start(); } public String date(int seconds) { Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); return simpleDateFormat.format(date); } }
- 假设我们的需求有了升级,不仅仅需要 2 个线程,而是需要 10 个,也就是说,有 10 个线程同时对应 10 个 SimpleDateFormat 对象。我们就来看下面这种写法:
/** 利用了一个 for 循环来完成这个需求。 for 循环一共循环 10 次,每一次都会新建一个线程 每一个线程都会在 date 方法中创建一个 SimpleDateFormat 对象 可以看出一共有 10 个线程,对应 10 个 SimpleDateFormat 对象。 代码的运行结果: 00:00 00:01 00:02 00:03 00:04 00:05 00:06 00:07 00:08 00:09 */ public class ThreadLocalDemo02 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { int finalI = i; new Thread(() -> { String date = new ThreadLocalDemo02().date(finalI); System.out.println(date); }).start(); Thread.sleep(100); } } public String date(int seconds) { Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); return simpleDateFormat.format(date); } }
- 需求变成了 1000 个线程都要用到 SimpleDateFormat
但是线程不能无休地创建下去,因为线程越多,所占用的资源也会越多。假设我们需要 1000 个任务,那就不能再用 for 循环的方法了,而是应该使用线程池来实现线程的复用,否则会消耗过多的内存等资源。public class ThreadLocalDemo06 { public static ExecutorService threadPool = Executors.newFixedThreadPool(16); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { String date = new ThreadLocalDemo06().date(finalI); System.out.println(date); } }); } threadPool.shutdown(); } public String date(int seconds) { Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("mm:ss"); } }; }
- 假设有个需求,即 2 个线程都要用到 SimpleDateFormat。代码如下所示:
-
保存一些业务内容
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息,日志信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
二、ThreadLocal 是用来解决共享资源的多线程访问的问题吗
- 不是,ThreadLocal 并不是用来解决共享资源问题的。虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。
- ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。
三、多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的
-
Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
- 每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量
- 这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。
- key 就是 ThreadLocal 的引用
- value 这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等
-
重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。
-
源码分析
- get 方法
public T get() { //获取到当前线程 Thread t = Thread.currentThread(); //获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象 ThreadLocalMap map = getMap(t); if (map != null) { //获取 ThreadLocalMap 中的 Entry 对象并拿到 Value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //如果线程内之前没创建过 ThreadLocalMap,就创建 return setInitialValue(); }
- getMap 方法
这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 nullThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
- set 方法
- 首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap
- 如果 map == null 则去创建这个 map
- 而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去
- map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
- ThreadLocalMap 类,也就是 Thread.threadLocals
- ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量
- 其中最重要的就是截取出的这段代码中的 Entry 内部类。
- 在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。
- 我们可以把 Entry 理解为一个 map,其键值对为:
- 键:当前的 ThreadLocal
- 值:实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
- ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。
- 但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同:
- HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链
- ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; //... }
- get 方法
四、内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()
-
什么是内存泄漏
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。 -
Key 的泄漏
- 线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。
- 我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。
- GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
- JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
-
Value 的泄漏
- 仔细看上面Entry代码,发现虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用
- 如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收
- 但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题
-
如何避免内存泄露
调用 ThreadLocal 的 remove 方法public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }