首页 > 系统相关 >从ThreadLocal底层源码一直聊到内存泄漏,干货满满!!

从ThreadLocal底层源码一直聊到内存泄漏,干货满满!!

时间:2024-07-16 20:59:45浏览次数:12  
标签:ThreadLocalMap threadLocal value 干货 ThreadLocal 源码 线程 内存

小强最近在疯狂补习高并发的相关知识,但是在学到threadLocal时有点力不从心了,尤其是threadLocal的底层架构和如何导致内存泄漏,今天我们帮小强一把!!把这一块彻底聊清楚!!!


文章目录

1.threadLocal的前世今生

1.为什么要使用threadLocal

ThreadLocal,即线程本地变量,在类定义中的注释如此写This class provides thread-local variables。如果创建了一个ThreadLocal变量,在每次set的时候其实会设置到线程的本地内存,多个线程操作这个变量的时候,实际是在操作线程自己本地内存里面的变量,所以不会发生影响,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。属于空间换时间的解决线程安全问题的方案。

2.threadLocal和Synchonized的比较

ThreadLocal 和 Synchonized 都用于解决多线程并发访问。可是 ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制, 使共享资源某一时该仅仅能被一个线程访问。
ThreadLocal 则是副本机制,一个线程会存储当前变量的副本,线程与线程之间都是隔离的,所以此时不管有多少线程访问都是并发安全的。但是可能会有内存泄漏的风险。

3.使用场景

1.跨方法实现数据传递。
2.在web容器中,每个完整的请求周期会由一个线程来处理,因此可以在线程统一设置,但是并不是增加方法参数,想要用的时候直接获取即可。
3.在spring中用threadLocal来设计TransactionSynchronizationManager,利用切面实现了事务管理和数据访问的解耦,同时也保证了多线程情况下Connection安全问题。(这个在之后spring源码会讲到!!)

类的解释如下

public abstract class TransactionSynchronizationManager {

    private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

    private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");

    private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
    new NamedThreadLocal<>("Current transaction read-only status");

    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
    new NamedThreadLocal<>("Current transaction isolation level");

    private static final ThreadLocal<Boolean> actualTransactionActive =
    new NamedThreadLocal<>("Actual transaction active");

2.ThreadLocal的使用

1.接口方法

ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除, 目的是为了减少内存的占用。
• protected Object initialValue()

protected T initialValue() {
    return null;
}

用protected修饰,表示可以被子类覆写,这个方法的调用时机实在当调用get()方法的时候,此时还没有设置值,那么返回initialValue()方法里return的值。

2.常见使用

public class ThreadLocalDemo {  
  
    // 创建一个ThreadLocal变量,用于存储每个线程的本地变量  
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();  
  
    public static void main(String[] args) {  
        // 创建一个线程池  
        ExecutorService executor = Executors.newFixedThreadPool(3);  
  
        // 提交三个任务到线程池  
        for (int i = 0; i < 3; i++) {  
            final int taskId = i;  
            executor.submit(() -> {  
                // 为当前线程设置ThreadLocal变量的值  
                threadLocal.set(taskId);  
                  
                // 模拟任务执行  
                try {  
                    Thread.sleep(1000); // 休眠1秒以模拟任务执行时间  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                  
                // 打印当前线程设置的ThreadLocal变量的值  
                System.out.println(Thread.currentThread().getName() + " 的 ThreadLocal 变量值为:" + threadLocal.get());  
                  
                // 清理ThreadLocal变量,避免内存泄漏(在实际应用中,这一步可以根据需要来决定是否执行)  
                threadLocal.remove();  
            });  
        }  
  
        // 关闭线程池  
        executor.shutdown();  
    } 
}

重头戏来了,我们先去看一下threadLocal的底层架构!!!

3.threadLocal的底层机制

1.线程模型图

1704263353273.png
从这个图可以看到,每一个线程都会维护一个threadLocalMap,threadLocalMap的key存的就是threadLocal的引用,value是设置的值。threadLocal实现线程本地关键正是由于每个线程维护了threadLocalMap变量。

2.ThreadLocal关键方法

public class ThreadLocal<T> {

   //get方法 
   public T get() {
        Thread t = Thread.currentThread();
        //返回当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //在threadLocalMap中存的是Object,在这个地方统一对泛型做转换
                T result = (T)e.value;
                return result;
            }
        }
        //对于首次获取来讲,map是为空的,因此会去调用初始化方法setInitialValue
        return setInitialValue();
    }

    //设置初始值
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //其实就是当前调用get()的对象
            map.set(this, value);
        else
            //在当前线程中创建ThreadLocalMap
            createMap(t, value);
        return value;
    }

    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    } 

	//set方法
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    	
     private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //之前已经设置过值,则覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
            	//之前没设置过值,则会设置值
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
}

3.ThreadLocalMap

ThreadLocalMap是一个声明在ThreadLocal的静态内部类,同时他的引用会在thread中维护。
ThreadLocalMap的结构

//
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
        	//类似于key-value的格式,v为Object
            //因为自己定义的ThreadLocal是可以接收任意类型的,只要在拿的时候根据泛型做转换即可
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
    }

         /**
         * 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;
    
}

接下来,我们去看下最重要的一个问题,threadLocal如何导致内存泄漏??

在这里插入图片描述

4.threadLocal内存泄漏

1.在什么情况下会出现内存泄漏

在实际项目开发中,一般都是使用线程池来创建任务,但是线程池最大的特点是线程池中的核心线程在执行完任务后,是不会退出的,可以循环使用。那此时,比如你在用线程池执行任务的时候,用threadLocal设置了一个值,但是运行完之后,并没有remove,当第二个任务来的时候,又用了这个线程,但是用的是另外一个threadLocal。那这种情况下之前的threadLocal会一直存在。
举个例子,大家仔细看下:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        executorService.execute(() -> {
            threadLocal.set(1);
        });

        Thread.sleep(1000);

        executorService.execute(() -> {
            threadLocal2.set(2);

            Integer i = threadLocal2.get();
            System.out.println(i);
        });

        }
    }

上面的demo,用的线程池只有一个线程,确保内存泄漏能复现。
从上面的代码上看,执行第二个任务的时候,你会发现threadLocalMap里面存在第一次线程执行完遗留的threadLocal。可以dubug看下,结果如下:
1721054789010.png
你会发现,执行第二个任务的时候,threadLocalMap中还存在执行第一次任务遗留的threadLocal,那此时,
由于threadLocal是ThreadLocalMap的key,ThreadLocalMap和thread是同周期,因此,只要核心线程一直存在,Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 就会一直存在,但是ThreadLocalMap的key(threadLocal)由于继承了弱引用,所以 threadlocal 将会被 gc 回收,那此时的value是永远访问不到的,所以会存在内存泄漏
最好的做法是不在需要使用ThreadLocal 变量后,都调用它的 remove()方法,清除数据,为了让大家理解清晰一些,我把对象关联图画下:1721055617876.png
从图中可以看出,一旦发生gc,key->threadLocal这条线就会断掉,key为null,那此时value就永远访问不到了。

2.为什么使用弱引用而不是强引用

强引用一定会发生内存泄漏,弱引用可能会发生内存泄漏,为什么呢?请往下看!!!
假如key 使用强引用:以上面的单线程线程池执行任务来看,第一次执行任务遗留的threadLocal肯定会到第二次执行任务的时候还存在,因此这部分遗留下来的 threadLocal就会发生内存泄漏。
假如key 使用弱引用: 对 ThreadLocal 对象实例的引用被被置为 null 了,由于ThreadLocalMap 持有 ThreadLocal 的弱引用, 即使没有手动删除, ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get ,remove 都 有机会被回收。

所以综合上述来讲
利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。 通过JVM 利用调用 remove 、get 、set 方法的时候,可以有效的去回收key为null的引用,减少内存泄漏的风险。当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,并没有去有调用 remove 、get 、set ,且线程没执行完,就有可能会有内存风险。

标签:ThreadLocalMap,threadLocal,value,干货,ThreadLocal,源码,线程,内存
From: https://blog.csdn.net/likang_1167/article/details/140474268

相关文章

  • 基于SpringBoot+Uniapp的微信阅读小程序系统设计与实现(源码+lw+部署文档+讲解等)
    文章目录前言项目运行截图技术框架后端采用SpringBoot框架前端框架Vue可行性分析系统测试系统测试的目的系统功能测试数据库表设计代码参考数据库脚本为什么选择我?获取源码前言......
  • ROS源码学习分享_6_ConnectionManager
        在上一章中,我们观察了PollManager节点背后的一些行为逻辑,但还有一些地方与本章有一些关联而没有讲到,这次我们就补上这些拼图。(本文章源自作者对于源码的观察理解以及其他资料的学习结合后的产物,仅用于自我复习,如有错误敬请见谅)    按照惯例我们先看一下......
  • webpack2源码架构设计与构建流程
    入口初始化入口文件lib/webpack.jsfunctionwebpack(options,callback){ letcompiler; if(Array.isArray(options)){ compiler=newMultiCompiler(options.map(options=>webpack(options))); }elseif(typeofoptions==="object"){//......
  • HAL库源码移植与使用之FSMC (例子加思路与理解,万字良心保证你能听懂)
    FMC和FSMC是一样的东西,只是FMC更可控地址更多又可以驱动SDRAM,用法都一样!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!总结:其实fsmc更像是一个有着特定转换时序功能的寄存器,每个fsmc映射在芯片内存里的地址都有一个这样的寄存器,你往这个映射的地址里赋值,这个赋值信息先到达对应fsmc寄存器,他不会像普通寄存器一样直接控制......
  • HAL库源码移植与使用之驱动LCD屏
    LCD屏会有烧屏残影的风险,但因其价格便宜他非常适合用于单片机显示显示屏分为以下几种:他的组成部包含玻璃基板、背光、驱动IC等LCD接口的种类MCU很简单,连51单片机都能驱动,但无法频繁刷新,一般有着20几个引脚引出的就是MCU接口我们常用的是就是MCU,下面讲的也是LCD屏幕MCU驱动......
  • HAL库源码移植与使用之正点原子OLED使用解析
    正点原子的OLED是使用SSD1306来驱动的,并设计了多种通讯方式,通过背后的焊点来选择这里以正点原子开发板最常用的8080并口通讯来讲引脚定义各正点原子开发板对OLED的接线8080时序图发送数据示例代码voidoled_wr_byte(uint8_tdata,uint8_tcmd){ OLED_RS(cmd); /......
  • 基于Java+SpringBoot+Vue的创新创业教育中心项目申报管理系统的设计与开发(源码+lw+部
    文章目录前言项目背景介绍技术栈后端框架SpringBoot前端框架Vue数据库MySQL(MyStructuredQueryLanguage)具体实现截图详细视频演示系统测试系统测试目的系统功能测试系统测试结论代码参考数据库参考源码获取前言......
  • Stable Diffusion 模型推荐,干货满满,建议收藏!
    最近有很多小伙伴说,刚接触StableDiffusion想尝试AI绘图,但是打开C站面对这么多的模型不知道从何下手,而且模型内存还不小,外网下载速度也慢,一旦选择的模型不符合需求,就需要重新挑选和下载,折腾着一天的时间就过去了,不但丹没炼成,购买的梯子套餐反而没了。那么接下来,我将通过自......
  • 基于web的宠物商城设计与实现 毕业论文终稿+初稿+修改版论文+开题报告+答辩PPT+论文检
    !!!有需要的小伙伴可以通过文章末尾名片咨询我哦!!! ......
  • Springboot 校园安全通事件报告小程序-计算机毕业设计源码02445
    Springboot校园安全通事件报告小程序系统摘 要随着中国经济的飞速增长,消费者的智能化水平不断提高,许多智能手机和相关的软件正在得到更多的关注和支持。其中,校园安全通事件报告小程序系统更是深得消费者的喜爱,它的出现极大地改善了消费者的生活质量,同时,它还创造了一种快......