当前位置:首页 > 单片机 > 架构师社区
[导读]来自:码匠笔记 ThreadLocal 对于大家并不陌生,每个人应该多少都用过,或者接触过,那么你真的了解她吗?我也是今天才揭开了她的神秘面纱。 看完这篇文章你将 GET 如下知识点: 什么是 ThreadLocal? ThreadLocal 真的会导致内存溢出吗? ThreadLocal 源码浅

来自:码匠笔记


ThreadLocal对于大家并不陌生,每个人应该多少都用过,或者接触过,那么你真的了解她吗?我也是今天才揭开了她的神秘面纱。

看完这篇文章你将 GET 如下知识点:

  1. 什么是 ThreadLocal?
  2. ThreadLocal 真的会导致内存溢出吗?
  3. ThreadLocal 源码浅析
  4. ThreadLocal 最佳实践
  5. ThreadLocal.remove 解决的到底是什么问题?

ThreadLocal 是什么?

ThreadLocal字面意思是线程本地变量,那么什么是线程本地变量呢?他解决了什么问题?先看下面这个例子

public class ThreadLocalTest { public static void main(String[] args) {
        Task task = new Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
        }
    } static class Task { private int value; public int calc(int i) {
            value += i; return value;
        }
    }
}

内容很简单,启动 3 个线程,分别调用 calc 方法,然后打印线程名字和计算内容,输出如下:

Thread-0 : 10
Thread-1 : 20
Thread-2 : 30

结果不难分析,因为这么 3 个线程共用一个 Task对象,所以 value 内容会累加,那么结果是不是不是你预期呢?那么我们再看一个例子

public class ThreadLocalTest2 { public static void main(String[] args) {
        ThreadLocalTest2.Task task = new ThreadLocalTest2.Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
        }
    } static class Task {
        ThreadLocalvalue; public int calc(int i) {
            value = new ThreadLocal();
            value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
        }
    }
}

运行结果如下

Thread-0 : 10
Thread-1 : 10
Thread-2 : 10

这次结果就对了吧,把 value 修改成了 ThreadLocal,然后每个线程就不会互相影响内容了,那么为什么他可以做到呢?这就是  ThreadLocal的意义所在,他解决的就是线程私有变量,多线程不互相影响。我们去源码一看究竟

ThreadLocal 源码赏析

看源码最简单粗暴的方式就是从入口进行,我们直接看 ThreadLocal.set方法,她直接获取了当前线程,然后调用了 getMap(t),也就是当前线程的 threadLocals变量,如果没有直接调用 createMap创建,然后返回,那么看到这里我们就知道了,ThreadLocal就是一个工具类,让我们可以把内容通过k-v的方式设置在当前线程上面(里面实际是使用 ThreadLocalMap进行存储,秒看一下代码和 HashMap 原理非常相似),既然存储在当前线程上面那么当然不会有线程安全问题了,这就是线程本地变量的内容喽。

当然我们要尤为注意,key 是 this 也就是当前的 ThreadLocal对象,记住这点下文要说呢。

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 getMap(Thread t) { return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal 会内存溢出吗?

不过还没有结束,大家最爱谈了的就是 ThreadLocal 的内存溢出问题,那么她真的会内存溢出么?

我们再看一个例子,例子和刚才不同的地方是只使用了一个线程(也就是 Main 线程)循环运行示例,每次创建新的 Task 对象,我们可想而知,这样每次创建不同的 Task,只要线程不结束,会不停的往当前线程的 threadLocals里面 set 内容,因为每次都是新 Task ,自然 ThreadLocal也是新的,那么如果循环足够大,并且线程一直存在,肯定会内存溢出呢呀!!!我们自己动手试试才知道啊。

下面的例子中,在 i == 80 的时候做了一次强制 GC,我们直接 DEBUG 看下效果。

public class ThreadLocalTest3 { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Task().calc(10); if (i == 80) {
                System.gc();
            }
        }
    } static class Task {
        ThreadLocalvalue; public int calc(int i) {
            value = new ThreadLocal();
            value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
        }
    }
}

在 for 循环行的左侧点击 debug,然后点击右键,这样 DEBUG 会停留在循环变量 i 等于 79 和 81 的地方,循环 100 次是为了更好的查看效果。好了我们可以直接观察一下 i == 80 前后的运行情况了

i == 79 || i == 81

那么开始我的表演,DEBUG 分别停在了 79 和 81 的位置上面,我们直接运行一下当前线程的内容获取到 threadLocals的内容

Thread.currentThread().threadLocals

可以看到里面的 ThreadLocalMap 的 size 分别是 83 和 4,这说明了什么?GC的时候把 83-4 = 79 个 ThreadLocalMap的内容回收了?


好吧,那我们继续看下代码吧

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table; int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked")
                    ThreadLocalkey = (ThreadLocal) e.get(); if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value;

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

原来 ThreadLocal的 ThreadLocalMap里面存的每一个 Entry 是一个 WeakReference,WeakReference会在 GC 的时候进行回收,回收的其实是 key,也就是弱引用的 referent, 然后  ThreadLocal会在 set 和 get 的时候对 key 为空的 value 进行删除,所以这样就完美解决了当前线程生命周期不结束的时候,不同的 ThreadLocal不停的追加到当前线程上面,导致内存溢出。

等等,那我自己写个程序,遇到 GC 不是就获取不到 ThreadLocal对象了吗?不是的,因为一个对象只有仅仅被 WeakReference引用才会被回收。

哎,如果 work1 的引用不在了,并且 Entry 对 ThreadLocal 的引用是弱引用才会回收,是不是很巧妙的解决了这个问题?


所以 WeakReference解决的就是内存溢出问题,如果持有 ThreadLocal 对象被回收了,内存自然会被回收,如果 ThreadLocal 的对象一直存在不被回收,并不能称之为内存溢出。

ThreadLocal 最佳实践

千呼万唤始出来,因为 ThreadLocal这个特性,深受各种框架喜欢,比如 MyBatis,Spring 大量的使用的 ThreadLocal,下面是用一个最常用的案例说明一下,首先我有一个拦截器,每次请求来,使用当前的 sl 的内容 + 10,我是为了模拟效果,通常这个做法是用于传递当前登录态,以便一次请求在任何地方都可以轻松的获取到登录态。

public class SessionInterceptor implements HandlerInterceptor { public static ThreadLocalsl = new ThreadLocal(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        Integer value = sl.get(); if (value != null) {
            sl.set(value + 10);
        } else {
            sl.set(10);
        } return true;
    } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {

    } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {

    }
}

然后我在 controller 里面获取 ThreadLocal里面的内容,并打印当前线程的名称和内容

@RestController public class IndexController { @RequestMapping("/") public Integer test() {
        System.out.println(Thread.currentThread().getName() + " : " + SessionInterceptor.sl.get()); return SessionInterceptor.sl.get();
    }
}

接下来我们启动服务,运行我编写好的 Spring Boot Application

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class MainBootstrap { public static void main(String[] args) {
        SpringApplication.run(MainBootstrap.class, args);
    }
}

浏览器访问 https://localhost:8080,疯狂的刷新浏览器,控制台打印的效果如下

http-nio-8080-exec-1 : 10
http-nio-8080-exec-3 : 10
http-nio-8080-exec-4 : 10
http-nio-8080-exec-1 : 20
http-nio-8080-exec-2 : 10
http-nio-8080-exec-3 : 20
http-nio-8080-exec-4 : 20
http-nio-8080-exec-1 : 30
http-nio-8080-exec-2 : 20
http-nio-8080-exec-3 : 30
http-nio-8080-exec-4 : 30

呀,和我想象的不一样啊,我这可是浏览器的请求,不应该是每个请求一个线程,使用自己的 ThreadLocal 吗,怎么值也累加了?

别慌问题出现在这里,在池化技术流行的年代,自然 Tomcat 也用了池化基础,其实每个请求过来,是直接在 Tomcat 的线程池里面获取一个线程,然后运行,所以一个请求结束如果 ThreadLocal 的内容不重置,就会影响其他请求,想象如果你这个地方是做的用户登录的绑定,那么岂不是资源乱套了?

那么怎么解决呢?还记得刚才的 SessionInterceptor 类么,直接在里面的 afterCompletion添加 sl.remove()即可,意思是在请求结束的时候,把当前线程的私有变量删除,这样就不影响其他线程了。

网上的一些说这个操作是为了更好的 GC 回收没用的实例,如果不设置也会自动回收,其实是不对的。为了让上下文都可以获取到 ThreadLocal 的内容,所以比如是静态的 ThreadLocal 所以持有的引用一直存在,并不会被回收,所以其实是在恢复线程的状态,不影响其他请求。

@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        sl.remove();
    }

修改以后我们重新狂刷浏览器,是不是问题就解决了呢?好的如果你有任何关于 ThreadLocal 的问题欢迎给我留言其他讨论,如果有不对的地方也欢迎指正。对了所有文章中的代码,都可以在订阅号后台回复 “ThreadLocal” 获取。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

用了三年 ThreadLocal 今天才弄明白其中的道理

长按订阅更多精彩▼

用了三年 ThreadLocal 今天才弄明白其中的道理

如有收获,点个在看,诚挚感谢

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭