深入理解ThreadLocal及源码解析

ThreadLocal是什么
  要理解ThreadLocal是什么,会涉及到3个类:Thread、ThreadLocalMap、ThreadLocal。在我们常用的线程Thread类中,有定义一个变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

  ThreadLocalMap类是定义在ThreadLocal类中的内部类,我们通过操作ThreadLocal类中的set()、get()等方法,可以在线程生命周期内向这个map变量设置、获取值。

ThreadLocal有什么用
  1.假如在我们的程序执行链路中,很多方法都需要用到userId(比如记录日志),如果在每个方法都增加userId参数,这样的话代码会变得很臃肿,也不优雅。没错,可以使用ThreadLocal将userId放在当前线程中
  2.Spring处理事务时,会用到Connection对象,需要保证一个事务中获取到的都是同一个Connection对象,肯定不能在每个service方法中都添加Connection参数,那Spring是怎么实现的呢?没错,就是ThreadLocal
所以,以下这几个场景,可以考虑使用ThreadLocal:
    1)方法调用链路中,多个方法用到的参数,且方法不方便传输。比如session、userId、requestId等。
    2)线程数据隔离(创建一个变量,且这个变量只能当前线程可见)

ThreadLocal怎么使用
  ThreadLocal实际是将threadLocal对象作为key,值作为value,设置到当前线程的map中,所以一般使用时,会将threadLocal设置为静态变量或单例,确保同一个key才能获取到对应的value。
如果项目中出现类似于下面这种代码,会有什么问题?

public class ThreadLocalDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            threadLocal.set("hello");
            String value = threadLocal.get();
            System.out.println(value);
        }
    }
}

项目中代码一般都是由线程池中线程执行,运行完成后线程不会销毁,这些threadLocal对象和value会一直被当前线程引用,导致GC不掉,那不就产生内存泄漏问题了嘛。ThreadLocal通过对key的弱引用(下次GC只存在弱引用的对象会被回收掉)设计,且在每次调用set()、get()、remove()方法时,都会自动将key为空的Entry对象清理掉,巧妙的解决了这个问题。
ThreadLocal使用建议:
    1)每次使用完,都调用remove()方法。
    2)threadLocal对象设置为静态或单例,确保后面能根据同一个key获取到对应value

ThreadLocal源码解析
调用threadLocal.set()方法时源码解析:

public void set(T value) {
    // 1.获取当前线程
    Thread t = Thread.currentThread();
    // 2.获取当前线程上的ThreadLocalMap变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 3.将threadLocal作为key,值作为value,设置到map中
        map.set(this, value);
    else
        createMap(t, value);
}

向map.set(this, value)源码逻辑:

private void set(ThreadLocal key, Object value) {
    // 1.实际存放数据的Entry数组
    Entry[] tab = table;
    int len = tab.length;
    // 2.hash到数组对应下标
    int i = key.threadLocalHashCode & (len-1);
    // 3.处理hash碰撞,下标一直向右移动,直到找到空位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        // 3.1 同一个key,将value值覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        // 3.2 如果key已被回收,则进行替换处理
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 4.当前下标位置为空,new一个Entry,放在数组对应下标上
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 5.清理key为空的数据 && 如果必要,则进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

调用threadLocal.get()方法时源码解析:

public T get() {
    // 1.获取当前线程
    Thread t = Thread.currentThread();
    // 2.获取当前线程的ThreadLocalMap变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3.获取到当前key对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

map.getEntry(this)代码逻辑:

private Entry getEntry(ThreadLocal key) {
    // 1.hash到数组对应下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 2.获取到了key对应的Entry
        return e;
    else
        // 3.未获取到Entry,继续尝试获取
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss(key, i, e)方法逻辑:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 1.当前位置Entry不为空,但又属于当前key,说明此位置发生了hash碰撞。
    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // 1.1 清理key为空的数据
            expungeStaleEntry(i);
        else
            // 1.2 继续向右移动下标,尝试寻找Entry,对应set时hash碰撞逻辑。
            i = nextIndex(i, len);
            e = tab[i];
    }
    return null;
}

源码逻辑整理:
  1. key和value是保存在Entry数组中,且key是被弱引用。
  2. 设置值时,如果产生hash碰撞,一直寻找下一个空下标位置,将Entry放入数组,还会清理key被回收掉的Entry及判断是否需要扩容。
  3. 获取值时,如果key对应的数组下标位置存在Entry,且这个Entry属于当前key,则直接获取对应value即可;如果这个Entry不属于当前key,表示此位置存在hash碰撞,继续向右移动下标尝试获取Entry。同时也会清理移动下标碰到的key为空的Entry。

ThreadLocal内存泄漏问题
使用ThreadLocal,一定会讲到内存泄漏问题,下图是ThreadLocal数据结构:
在这里插入图片描述
也就是说,Thred中有一个ThreadLocalMap变量,ThreadLocalMap中有一个Entry[]数组变量,我们设置的值作为Entry的value,Entry的key弱引用threadLocal变量。两种情况:
    1.如果threadLocal定义的是线程变量,当线程运行完成,由于是线程池,线程会一直存活,会一直保持对threadLocal和value的引用,由于key采用弱引用,在下一次GC时,threadLocal对象会被回收,需要等到下一次对这个线程的threadLocal操作,才会断开对value的引用,那么这段时间中,value对象是一直不能被GC掉的,从而导致内存泄漏。试想一下,如果key是强引用,这种情况key和value就永远都不会被GC掉了,想想好可怕。
    2.如果threadLocal定义的是静态变量,那threadLocal对象会一直存活,在上面的场景下,value会一直被引用,导致value不会被GC,产生内存泄漏。
所以,当时使用完后,一定记得在finally中调用remove()方法,手动释放对key和value的引用,避免产生内存泄漏。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
├─第一阶段 │      源码+ppt.rar │      高并发编程第一阶段01讲、课程大纲及主要内容介绍.wmv │      高并发编程第一阶段02讲、简单介绍什么是线程.wmv │      高并发编程第一阶段03讲、创建并启动线程.mp4 │      高并发编程第一阶段04讲、线程生命周期以及start方法源码剖析.mp4 │      高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │      高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制抽取出来.mp4 │      高并发编程第一阶段07讲、策略模式在Thread和Runnable的应用分析.mp4 │      高并发编程第一阶段08讲、构造Thread对象你也许不知道的几件事.mp4 │      高并发编程第一阶段09讲、多线程与JVM内存结构的关系,虚拟机栈实验.mp4 │      高并发编程第一阶段10讲、Thread构造函数StackSize详细讲解.mp4 │      高并发编程第一阶段11讲、Thread构造函数StackSize详细讲解-续.mp4 │      高并发编程第一阶段12讲、Daemon线程的创建以及使用场景分析.mp4 │      高并发编程第一阶段13讲、线程ID,优先级讲解.mp4 │      高并发编程第一阶段14讲、Thread的join方法详细介绍,结合一个典型案例.mp4 │      高并发编程第一阶段15讲、Thread断Interrupt方法详细讲解.mp4 │      高并发编程第一阶段16讲、采用优雅的方式结束线程生命周期.mp4 │      高并发编程第一阶段17讲、Thread API综合实战,编写ThreadService实现暴力结束线程的综合实战.mp4 │      高并发编程第一阶段18讲、数据同步的引入与Synchronized的简单介绍.mp4 │      高并发编程第一阶段19讲、结合jconsole,jstack以及汇编指令认识synchronized关键字.mp4 │      高并发编程第一阶段20讲、同步代码块以及同步方法之间的区别和关系.mp4 │      高并发编程第一阶段21讲、通过实验分析This锁的存在.mp4 │      高并发编程第一阶段22讲、通过实验分析Class锁的存在.mp4 │      高并发编程第一阶段23讲、多线程死锁分析,案例介绍.mp4 │      高并发编程第一阶段24讲、线程间通信快速入门,使用wait和notify进行线程间的数据通信.mp4 │      高并发编程第一阶段25讲、多Produce多Consume之间的通讯导致出现程序假死的原因分析.mp4 │      高并发编程第一阶段26讲、多线程下的生产者消费者模型,以及详细介绍notifyAll方法.mp4 │      高并发编程第一阶段27讲、wait和sleep的本质区别是什么,深入分析(面试常见问题).mp4 │      高并发编程第一阶段28讲、线程生产者消费者的综合实战结合Java8语法.mp4 │      高并发编程第一阶段29讲、如何实现一个自己的显式锁Lock精讲上.mp4 │      高并发编程第一阶段30讲、如何实现一个自己的显式锁Lock精讲下(让锁具备超时功能).mp4 │      高并发编程第一阶段31讲、如何给你的应用程序注入钩子程序,Linux下演示.mp4 │      高并发编程第一阶段32讲、如何捕获线程运行期间的异常.mp4 │      高并发编程第一阶段33讲、ThreadGroup API介绍之一.mp4 │      高并发编程第一阶段34讲、ThreadGroup API介绍之二.mp4 │      高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │      高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │      高并发编程第一阶段37讲、给线程池增加拒绝策略以及停止方法.mp4 │      高并发编程第一阶段38讲、给线程池增加自动扩充线程数量,以及闲时自动回收的功能.mp4 │      高并发编程第一阶段39讲、课程结束,内容回顾,下季内容预告.mp4 │ ├─第二阶段 │       Java并发编程.png │       ppt+源码.rar │       高并发编程第二阶段01讲、课程大纲及主要内容介绍.wmv │       高并发编程第二阶段02讲、介绍四种Singleton方式的优缺点在多线程情况下.wmv │       高并发编程第二阶段03讲、介绍三种高效优雅的Singleto

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值