从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 ,且线程没执行完,就有可能会有内存风险。

  • 39
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值