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

前言:

当使用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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值