深入理解ThreadLocal原理 2024-06-26 18:12 ThreadLocal是一种用于实现**线程局部变量**的机制,它允许每个线程有自己独立的变量,从而达到了**线程数据隔离**的目的。 > 基于JDK8 ### 使用 通常在项目中是这样使用它的,创建线程变量工具类,然后用它去存取线程局部变量: ```java package com.example.demo.Utils; /** * @author: HanXu * on 2021/11/17 * Class description: 线程变量工具类 * 在每一个线程中储存一个变量,以记录当前线程生命流程中的动作 */ public class ThreadLocalUtil { private static final ThreadLocal<String> currentThreadLocal = ThreadLocal.withInitial(() -> new String()); /** * 获取值 * @return 当前线程中存放的变量值 */ public static String getCurrentThreadVal() { return currentThreadLocal.get(); } /** * set值 * @param value 唯一 */ public static void putCurrentThreadVal(String value) { currentThreadLocal.set(value); } /** * 清空当前线程中的数据 */ public static void clear() { currentThreadLocal.remove(); } } ``` 有了这个工具类,我们就能在线程执行时为每个线程放入不同的数据了,以下是一个小测试: ```java package com.example.demo.Utils; import java.util.Random; import java.util.concurrent.*; /** * @author: HanXu * on 2024/6/26 * Class description: 向10个线程中放入随机数,然后取出,查看放入取出是否一致 */ public class Test { private static final int THREAD_NUM = 10; private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM); public static void main(String[] args) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM); for (int i = 0; i < THREAD_NUM; i++) { threadPool.execute(() -> { Random t = new Random(); int num = t.nextInt(100); //等待所有线程都有任务再全部一起执行:在当前线程中放入100以内的随机数 waitOtherThread(); ThreadLocalUtil.putCurrentThreadVal(String.valueOf(num)); System.out.println(Thread.currentThread().getName() + ",放入的数字:" + num); countDownLatch.countDown(); }); } countDownLatch.await(); System.out.println(); System.out.println(); for (int i = 0; i < THREAD_NUM; i++) { threadPool.execute(() -> { //等待所有线程都有任务再全部一起执行:取出当前线程中存放的数字 waitOtherThread(); System.out.println(Thread.currentThread().getName() + ",取得数字:" + ThreadLocalUtil.getCurrentThreadVal()); ThreadLocalUtil.clear(); }); } threadPool.shutdown(); while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) { Thread.yield(); } System.out.println("执行完毕!"); } private static void waitOtherThread() { try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } ``` 执行结果: ``` pool-1-thread-10,放入的数字:80 pool-1-thread-8,放入的数字:22 pool-1-thread-7,放入的数字:52 pool-1-thread-6,放入的数字:99 pool-1-thread-5,放入的数字:91 pool-1-thread-3,放入的数字:26 pool-1-thread-4,放入的数字:25 pool-1-thread-1,放入的数字:64 pool-1-thread-2,放入的数字:56 pool-1-thread-9,放入的数字:38 pool-1-thread-9,取得数字:38 pool-1-thread-10,取得数字:80 pool-1-thread-8,取得数字:22 pool-1-thread-7,取得数字:52 pool-1-thread-6,取得数字:99 pool-1-thread-5,取得数字:91 pool-1-thread-3,取得数字:26 pool-1-thread-4,取得数字:25 pool-1-thread-1,取得数字:64 pool-1-thread-2,取得数字:56 执行完毕! ``` ### 原理 可以看到我们明明使用的是同一个ThreadLocal, 但作用到不同线程上就能**隔离他们之间的数据**。那ThreadLocal是如何做到线程间数据隔离的呢? 我们可以看下ThreadLocal.set的源码: ```java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ``` 可以看到通过getMap(t),得到了一个类似Map的对象ThreadLocalMap,然后向map中存入数据时,是以当前对象this为key存入的。当前对象this就是当前ThreadLocal对象,我们使用的是同一个ThreadLocal,所以this是一样的。 那就肯定是map不同,再看下getMap(t)怎么获取的: ```java ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ``` Thread t是当前线程,t.threadLocals就是获取的当前线程的threadLocals属性。 (在Thread类里有一个成员属性:ThreadLocal.ThreadLocalMap threadLocals = null;) 那它的原理就是**每个Thread内部有一个ThreadLocalMap类型的属性变量threadLocals**。然后每个线程执行ThreadLocal.set时,是**向自己的threadLocals中储存数据**。由于线程不同,所以threadLocals也就不同,达到了线程数据隔离的目的。而ThreadLocal只是用来操作当前线程中的ThreadLocalMap的工具类而已。所有的数据并没有放在ThreadLocal当中。 所以一句话总结就是:每个线程有自己的ThreadLocalMap,存取数据是从自己的ThreadLocalMap操作的。 ![](https://minio.riun.xyz/riun1/2024-06-26_64AFdAzhoKQjymsWoR.jpg) ### 细节 #### ThreadLocalMap ThreadLocalMap是一个Map,所以它也是数组+链表结构;但是由于我们使用的时候是一个ThreadLocal对象,而存数据时是以当前ThreadLocal对象作为key的,所以这个Map中只会有一个索引位置被使用,且不会有链表形成。(所以它内部并没有链表的实现) ![](https://minio.riun.xyz/riun1/2024-06-26_64zoDbxUTov5uI896h.jpg) #### Entry 我们再来看看Entry(ThreadLocal -> static class ThreadLocalMap -> static class Entry): ```java static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ``` 很简单的k,v键值对构成的对象,但是它却继承了弱引用WeakReference,让自己的k变成了弱引用类型:super(k); 为什么是弱引用的key我们后文再说。 #### remove 一般我们使用set存值,get取值,当这个变量不再使用了,我们需要手动remote()清除掉,ThreadLocal.remote(): ```java public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ``` ThreadLocal.ThreadLocalMap.remote(): ```java private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; //获取ThreadLocal作为key在数组中的索引下标 int i = key.threadLocalHashCode & (len-1); //虽然是循环,但是我们的使用方式数组中只会有一个索引有值 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //使用的父类的clear方法,即:把key置为null e.clear(); //把value置为null,通常也会把key为null的value置为null expungeStaleEntry(i); return; } } } ``` 我们在项目中使用ThreadLocal,在当前线程用完后,一定要手动remove(),这样在当前线程生命周期结束的时候,所有对象都会变为垃圾可回收。 #### 内存泄露 如果我们在使用完ThreadLocal后,不手动remove(),若当前线程生命周期还未结束,那线程会一直持有ThreadLocalMap的引用,而ThreadLocalMap引用Entry,Entry引用了对应的key,value,而我们使用完了ThreadLocal,ThreadLocal已经没有了,就永远无法再获取这个Entry的key,value,这样就会造成了内存泄露: ![](https://minio.riun.xyz/riun1/2024-06-26_64Dc0kuuuuOgerYv2m.jpg) 所以正确的使用方法是:定义一个ThreadLocal,在线程运行时使用它,在使用完成之后执行remove()。 另外我们项目中一般都是用static final修饰ThreadLocal也是因为我们在项目运行期间只想要一个ThreadLocal对象,这样当发生一些不可预料的事情时,由于我们只有一个ThreadLocal对象,所以我们也能够操作之前这个位置的Entry内容,修改或删除它。 #### Why WeakReference? 通过上图我们可以看到,无论Entry的key使用强引用还是弱引用,**如果没有remove()**,那**在线程生命周期没有结束时**,都是会造成内存泄露的。那为什么要使用弱引用呢? 因为ThreadLocal的set / get /remove方法执行时,都会做一些额外的事:将key为null的Entry里的value也置为null: ```java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) //在这个方法内部 map.set(this, value); else createMap(t, value); } ``` 这样,假设我们在某个方法中定义了ThreadLocal,使用完未remove();而当我们下次在另一个方法中再定义一个ThreadLocal时,进行set / get /remove任意操作,都会将当前线程内ThreadLocalMap中key为null的Entry进行清除,将其value也置为null,将一个本该内存泄露的对象变为了可回收的垃圾。 这样相当于多了一层保障,从而减少内存泄露发生的可能: ![](https://minio.riun.xyz/riun1/2024-06-26_64Dy7TiYmNWwuJfzeq.jpg) 可能有些同学注意到我上面强调了在线程生命周期内,这是因为如果线程生命周期结束了,则不管有没有remove(),对应的Entry一定会被作为垃圾回收。因为线程生命周期结束后,ThreadLocal和ThreadLocalMap都没有了,没有栈引用指向堆空间这些对象,所以他们都是垃圾可以被回收。 而在我们进行系统开发时,这点通常是不好控制的,因为有可能许多请求并发访问时,请求的线程都没有执行结束,所以如果我们不remove(),那一点点内存泄露就有可能导致内存溢出。 综上,ThreadLocal导致内存泄露的两个原因就是: 1、使用完没有remove() 2、使用完ThreadLocal后,线程生命周期并没有结束 所以要解决ThreadLocal的内存泄露问题,只需要满足任意一个即可: 1、一定要remove() 2、使用完ThreadLocal后,线程结束 但是第2点我们不好控制,所以一般都是使用第1点。 #### 丢失ThreadLocal? 还有人可能在乎的点是:把key作为弱引用,发现即回收,若GC执行时发现了这个ThreadLocal那它不就被回收了吗?那我们程序执行不就出现NPE了吗? 其实不是的,因为我们还有一个自己定义的ThreadLocal threadLocal = new ThreadLocal()这个强引用在指向该ThreadLocal,所以在使用期间这个ThreadLocal是不会被垃圾回收的。 ### 框架中的应用 Spring的事务管理器使用的ThreadLocal,SpringMVC的HttpSession、HttpServletRequest、HttpServletResponse都是放在ThreadLocal中的,以保证线程安全。 ### 总结 因此,当我们想要隔离线程变量时,可以使用ThreadLocal,但是使用时要注意,一般定义一个static final的ThreadLocal,且使用完之后要一定记得remove(); 另外ThreadLocal还有很多变种,比如InheritableThreadLocal和**TransmittableThreadLocal**。想要更多了解使用的可以看这篇文章:[全链路追踪traceId](https://riun.xyz/work/2507723) --END--
发表评论