ThreadLocal的结构、使用以及内存泄露问题

关于ThreadLocal的认知一直停留在了解阶段,有很多细节和具体结构一直不是特别清楚,网上的博客写的过于公式话也描述的不是很清晰,所以查看源码来学习一下

首先先介绍一些将会出现的一些参数

  1. ThreadLocal : 其存在意义并不是作为存储数据的数据结构,而是作为一个工具类和索引key值来查询数据使用
  2. ThreadLocalMap : 真正存储数据的数据结构,采用map结构,定义是ThreadLocal的一个内部类,每个线程都有一个独立的ThreadLocalMap对象
  3. Entry : ThreadLocalMap中存储数据的节点,key-value结构,key为ThreadLocal,value为存储的数据

ps.内部类不代表对象在ThreadLocal的内部,也不代表ThreadLocal new实例的时候会在内部new 出来ThreadLocalMap对象

这里我们要明确的就是存储数据的是每个线程自己的ThreadLocalMap而不是ThreadLocal。

放一张我自己做的图,主要为了表示引用关系和数据结构所以并没有体现出堆栈环境。

在这里插入图片描述
再放一张网上比较常见的图
在这里插入图片描述

只看图可能不太理解我们来结合使用场景来进行一些介绍

实际应用

一般来讲,我们使用ThreadLocal带来的好处就是隔离性,将各个线程的数据隔离开来自然不存在并发问题,其次使用ThreadLocal的另一个好处就是可以在线程内的任何地方获得数据

我们考虑一下以下场景,在我们的现场工作过程中可能会产生若干方法调用,如果我们需要向方法中传入状态的话首先想到的应该是传参的方式

	public void method(String status){
        work(status);
        send(status);
        close(status);
        log(status);
    }

那如果方法调用中仍存在方法调用且仍需要状态信息呢?

	public void work(String status){
        work1(status);
        work2(status);
        work3(status);
    }

我们可能向work()方法中传参只是为了传递给work方法里面的函数,那如果使用这种传参的方式未免太过笨重?

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,状态信息就传不进去了。

这个时候我们就想要一个在同一个线程里面可以由各个方法直接拿到而不用传参的一个数据,这也就是我们Java标准库提供的一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。

通常我们在使用ThreadLocal时时以静态字段初始化的

private static ThreadLocal<Object> sessionThreadLocal = new ThreadLocal<Object>();

代码实例如下

package learn.threadLocalDemo;

public class ThreadLocalDemo {
    private static ThreadLocal<Object> sessionThreadLocal = new ThreadLocal<>();
    private static ThreadLocal<Object> connectionThreadLocal= new ThreadLocal<>();
    private static ThreadLocal<Object> statusThreadLocal= new ThreadLocal<>();


    public static class ThreadDemo implements Runnable{
        private String name;
        private String sessionStatus;
        private String connectionStatus;
        private String status;
        ThreadDemo(String name,String sessionStatus,String connectionStatus,String status){
            this.name = name;
            this.sessionStatus = sessionStatus;
            this.connectionStatus = connectionStatus;
            this.status = status;
        }
        @Override
        public void run() {


            for (int i = 0; i < 3; i++) {
                if(null == ThreadLocalDemo.sessionThreadLocal.get()){
                    sessionThreadLocal.set(sessionStatus);
                    System.out.println("置入线程" + name + "的sessionThreadLocal值为" + sessionStatus);
                }else {
                    String session = (String)sessionThreadLocal.get();
                    System.out.println("线程" + name + "的sessionThreadLocal值为" + session);
                }
                if(null == ThreadLocalDemo.connectionThreadLocal.get()){
                    connectionThreadLocal.set(connectionStatus);
                    System.out.println("置入线程" + name + "的sessionThreadLocal值为" + connectionStatus);
                }else {
                    String connection = (String)connectionThreadLocal.get();
                    System.out.println("线程" + name + "的sessionThreadLocal值为" + connection);
                }
                if(null == ThreadLocalDemo.statusThreadLocal.get()){
                    statusThreadLocal.set(status);
                    System.out.println("置入线程" + name + "的sessionThreadLocal值为" + status);
                }else {
                    String status = (String)statusThreadLocal.get();
                    System.out.println("线程" + name + "的sessionThreadLocal值为" + status);
                }
            }


        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadDemo("demo1","session1","connection1","status1")).start();
        new Thread(new ThreadDemo("demo2","session2","connection2","status2")).start();
        new Thread(new ThreadDemo("demo3","session3","connection3","status3")).start();


    }
}

输出结果为

置入线程demo2的sessionThreadLocal值为session2
置入线程demo2的sessionThreadLocal值为connection2
置入线程demo2的sessionThreadLocal值为status2
置入线程demo3的sessionThreadLocal值为session3
置入线程demo1的sessionThreadLocal值为session1
置入线程demo3的sessionThreadLocal值为connection3
线程demo2的sessionThreadLocal值为session2
置入线程demo3的sessionThreadLocal值为status3
置入线程demo1的sessionThreadLocal值为connection1
线程demo3的sessionThreadLocal值为session3
线程demo2的sessionThreadLocal值为connection2
线程demo2的sessionThreadLocal值为status2
线程demo2的sessionThreadLocal值为session2
线程demo2的sessionThreadLocal值为connection2
线程demo3的sessionThreadLocal值为connection3
置入线程demo1的sessionThreadLocal值为status1
线程demo1的sessionThreadLocal值为session1
线程demo3的sessionThreadLocal值为status3
线程demo2的sessionThreadLocal值为status2
线程demo3的sessionThreadLocal值为session3
线程demo1的sessionThreadLocal值为connection1
线程demo3的sessionThreadLocal值为connection3
线程demo1的sessionThreadLocal值为status1
线程demo1的sessionThreadLocal值为session1
线程demo3的sessionThreadLocal值为status3
线程demo1的sessionThreadLocal值为connection1
线程demo1的sessionThreadLocal值为status1

源码探究

在开始了解ThreadLocal的过程中总是对其整体的数据结构很混乱,分不清ThreadLocal和ThreadLocalMap,所以通过阅读源码来深入了解其结构和机制

首先我们从set方法入手看ThreadLocal是如何设置值的

 public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);//如果map不为空,置入值,key为本ThreadLocal,value为传入参数
        else
            createMap(t, value);//如果map为空,new一个ThreadLocalMap,赋值给Thread的ThreadLocals属性,再置入值
    }

下面展示以上几个方法的内容

	ThreadLocalMap getMap(Thread t) {//返回Thread的threadlocals属性值
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {//创建ThreadLocalMap并置入值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

	//核心set方法
 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;//获取ThreadLocalMap的Entry数组table
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//定位到key值应该存放的位置

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {//遍历对应位置的Entry节点
                ThreadLocal<?> k = e.get();

                if (k == key) { //如果key值和当前要置入的key相同,修改value值即可
                    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();
        }

由上述代码我们可以看到,对于数据的以实际操作都是针对于ThreadLocalMap进行的,ThreadLocal仅仅只作为一个索引一个代理工具,作为Entry的key来使用

ps.此处两个set方法,一个只有value的是ThreadLocal的,有key有value的是ThreadLocalMap的

顺便贴上get,remove方法,其余的诸如ThreadLocalMap的具体代码由于过多此处不贴出来了,有兴趣的自己看看吧

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

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

关于内存泄露问题和线程池中使用ThreadLocal的注意事项

首先来讨论一下内存泄漏这个问题

这个问题主要产生于Entry中的key是一个弱引用

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object). Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table. Such entries are referred to
 * as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;
     Entry(ThreadLocal<?> k, Object v) {
     super(k);
     value = v;
     }
}

我们考虑这种设计下的ThreadLocal的回收情况

正常情况下,ThreadLocal存在一个静态强引用和Entry中对它的弱引用,也就是说,强引用存在的情况下ThreadLocal是不会被回收的

那么,当我们不再需要ThreadLocal的时候需要对其进行回收,对应的操作也就是将强引用ThreadLocal = null置空

如果Entry中对ThreadLocal的引用为强引用的话会导致我们很难回收掉ThreadLocal(引用太多需要一个一个null),所以为了回收方便,才将Entry中的引用设置为弱引用

但是这样的设置也有相应的问题,ThreadLocal是回收掉了,那value咋整?ThreadLocal一回收,它对应的Entry就变成(null,value)了,这个value永远也访问不到,且value是强引用,也就是说这个value我们永远也回收不掉了,也就是出线了内存泄漏

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value

但是这些被动的预防措施并不能保证不会内存泄漏,因为如果我们不去调用get()、set()、remove()方法,就不会触发清除,这块内存也就会一直存在,当然,如果线程结束了触发了整个线程的回收还是可以回收掉的,只是在ThreadLocal = null之后,线程回收之前会产生内存泄露问题

关于线程池中使用ThreadLocal

上面我们所讨论的最终结论是只是在ThreadLocal = null之后,线程回收之前会产生内存泄露问题。那么如果我们使用线程池的话,核心线程是不会被回收掉而会循环利用的,那么恰巧这个线程之后的任务都不会再调用get()、set()、remove()方法,这就会产生真正的内存泄露了,(null,value)的Entry会真正的永远无法回收掉。

所以我们在使用线程池中的线程使用ThreadLocal的时候,一定要在任务结束之前回收掉这个任务期间使用的ThreadLocal变量

try {
    threadLocal.set("xxx");
    ...
} finally {
    threadLocal.remove();
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值