[ThreadLocal]源码分析和内存溢出问题

    技术2022-07-13  82

    前言:

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    ThreadLocal 适用于如下两种场景

    每个线程需要有自己单独的实例实例需要在多个方法中共享,但不希望被多线程共享

    1. 类的内部方法和属性

    2. 简单地使用场景

    ThreadLocal<T>其实是与线程绑定的一个变量。所以我们的使用方式是,通过切面登录,然后就把比如用户信息放到redis中。

    然后进来的请求可以根据令牌信息然后从缓存拿用户信息,放到ThreadLocal中,方便后面直接获取这个线程所拥有的的用户信息。

    public class DemoThreadLocal { private static final ThreadLocal<HashMap> threadLocal = ThreadLocal.withInitial(() -> new HashMap()); // public static void setMap(HashMap map) { // threadLocal.set(map); // } private static HashMap getMap() { return threadLocal.get(); } public static void remove() { threadLocal.remove(); } public static String getCurGlobalRequestId() { return null == getMap().get("globalRequestId") ? "" : (String) getMap().get("globalRequestId"); } public static void setCurGlobalRequestId(String globalRequestId) { getMap().put("globalRequestId", globalRequestId); } }

    其实就是相当于吧ThreadLocal当做一个线程隔离的变量 map来使用。 

    public class Test { private static final ThreadLocal<HashMap> threadLocal = ThreadLocal.withInitial(() -> new HashMap()); public static void main(String[] args) { threadLocal.get(); threadLocal.get().put("a","a"); threadLocal.get().put("b","b"); System.out.println(threadLocal.get().get("a")); Thread t = Thread.currentThread(); } }

    一般来说有remove,get 和set比较常用。

    这边也是用了HashMap来存取一些比如用户信息之类的。

    ThreadLocal本身是跟并发有关的,但是很多情况确实如上述为了方便传参使用的。

    每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。  

    ThreadLocal应该尽量设计在一个全局的设计上,不应该是一种打补丁的间接方法。

    比如:

    Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。 为什么要放在ThreadLocal里面呢?由于Spring在AOP后并不能向应用程序传递參数。应用程序的每一个业务代码是事先定义好的,Spring并不会要求在业务代码的入口參数中必须编写Connection的入口參数。此时Spring选择了ThreadLocal,通过它保证连接对象始终在线程内部,不论什么时候都能拿到,此时Spring很清楚什么时候回收这个连接,也就是很清楚什么时候从ThreadLocal中删除这个元素

    向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。(下面源码分析时候发现set其实会把Thread中的ThreadLocalMap属性当做一个副本)

    一般的Web应用划分为控制层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

    所以场景中,可以:

    // 非线程安全 public class TopicDao { //①一个非线程安全的变量 private Connection conn; public void addTopic(){ //②引用非线程安全变量 Statement stat = conn.createStatement(); … } } import java.sql.Connection; import java.sql.Statement; public class TopicDao { //①使用ThreadLocal保存Connection变量 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection(){ //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, //并将其保存到线程本地变量中。 if (connThreadLocal.get() == null) { Connection conn = ConnectionManager.getConnection(); connThreadLocal.set(conn); return conn; }else{ //③直接返回线程本地变量 return connThreadLocal.get(); } } public void addTopic() { //④从ThreadLocal中获取线程对应的 Statement stat = getConnection().createStatement(); } }

    可以让多个Dao共用一个Connection,同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

    交给线程来管理,那么这个也是Spring对有状态类线程安全化的解决思路。

     

    3. 源码分析

    3.1 set(T value)

    这里面涉及到了Thread源码,还有ThreadLocalMap作为ThreadLocal的内部类。后面再读一下。

    /** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. * 常用可能用于让线程存储value */ public void set(T value) { //找到当前线程 Thread t = Thread.currentThread(); //从ThreadLocalMap中 获取这个t,也可以说这个是每个Thread类都会保存的一个ThreadLocalMap ThreadLocalMap map = getMap(t); //没有从Thread类中取得到map if (map != null) map.set(this, value); else //初始化这个map createMap(t, value); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { /** * 这个Thread类中 ThreadLocal.ThreadLocalMap threadLocals = null; * */ return t.threadLocals; } /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map * 初始化操作 */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

    3.2 get()

    返回当前线程所对应的线程局部变量。

    如果一个线程第一次调用threadLocal.get()方法时,此时拿到的map是null,会调用setInitialValue()

    /** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { //当前线程 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //map的entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //获得result T result = (T)e.value; return result; } } //如果为空 return setInitialValue(); } /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { //这边应该是返回一个空值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } /** * @return the initial value for this thread-local */ protected T initialValue() { return null; } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

    这边读的时候有一个很困惑的点,如果get它发现没有初始化这个ThreadLocalMap的话,它会放入一个空值,

    然后放到ThreadLocalMap里面并初始化。

    原因是因为,这个value是可以重写的,如果我们没有重写,那么会返回一个null。 T value = initialValue();

    比如(借用了深入学习java源码之ThreadLocal.get()()与ThreadLocal.initialValue()的代码):

    public class test { public static void main(String[] args) throws InterruptedException { new A().start(); new A().start(); new A().start(); new A().start(); } static class A extends Thread { static List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<List<Integer>>() { @Override protected List<Integer> initialValue() { return list; } }; @Override public void run() { List<Integer> threadList = threadLocal.get(); threadList.add(threadList.size()); System.out.println(threadList.toString()); } } }

    输出结果是:

    [1, 2, 3, 4, 5, 5] [1, 2, 3, 4, 5, 5, 6] [1, 2, 3, 4, 5, 5, 6, 7] [1, 2, 3, 4, 5, 5, 6, 7, 8]

    所以,这个方法其实更多是为了让我们去重载这个get的初始化的。

    3.3 remove()

    将当前线程局部变量的值删除,目的是为了减少内存的占用。

    更为了防止内存泄漏。

    当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

    /** * Removes the current thread's value for this thread-local * variable. If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim. This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */ public void remove() { //获取map ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

    对于ThreadLocalMap(ThreadLocal的内部类)类中的remove方法:

    /** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }

    相当于移除一个,ThreadLocal<?> key。

    一个线程可以有多个ThreadLocal,根据不同的key作为区分。

    expungeStaleEntry当在搜索过程中遇到了脏entry的话就会调用该方法去清理掉脏entry。

    具体可以读读:从源码深入详解ThreadLocal内存泄漏问题

    3.4  initialValue()

    protected T initialValue() { return null; }

     

    4. 问题?

    4.1 子线程能否访问父线程的threadlocal呢?

    threadLocal可以做线程级的数据隔离,那如何在子线程中获取父线程的值呢? 可以使用InheritableThreadLocal

    说明在子线程和孙线程中可以获取到父线程的 inheritableThreadLocal 的值。修改inheritableThreadLocal 的值后,子线程和孙线程中同样可以获取到父线程的inheritableThreadLocal 的值。

    但是threadLocal却不行,获取到的值为空。

    public class Test { public static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws Exception { inheritableThreadLocal.set("inheritableThreadLocal hello"); threadLocal.set("threadLocal world"); new Thread(()->{ System.out.println(String.format("子线程可继承值:%s", inheritableThreadLocal.get())); System.out.println(String.format("子线程值:%s", threadLocal.get())); new Thread(()->{ System.out.println(String.format("孙线程可继承值:%s", inheritableThreadLocal.get())); System.out.println(String.format("孙线程值:%s", threadLocal.get())); }).start(); }).start(); } }

    执行结果:

    子线程可继承值:inheritableThreadLocal hello 子线程值:null 孙线程可继承值:inheritableThreadLocal hello 孙线程值:null

    4.2 内存泄漏?

    由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生 命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收, Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

    由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key,无论key是强引用与弱引用,都会导致内存泄漏。但是使用弱引用可以多一层保障:弱引用能保证ThreadLocal对象能够保证被回收

    内存泄露问题解决办法: 

    存在内存泄露问题,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

     

    参考:

    深入学习java源码之ThreadLocal.get()()与ThreadLocal.initialValue()

    ThreadLocal使用场景分析

    超牛逼:https://www.jianshu.com/p/dde92ec37bd1

    Processed: 0.011, SQL: 9