ThreadLocal 原理、源码、内存泄漏

一、ThreadLocal

1、ThreadLocal是线程内部一个成员变量,它只属于当前线程,对其他线程不可见。

2、线程维护的是ThreadLocal的副本变量,每个线程操作自己的副本变量互不影响,所以不用考虑线程安全性问题。

3、简单的理解就是同一个ThreadLocal在不同的线程里调用get()方法得到的value不一样。

4、它实现的是同一个线程间的数据共享(隔离的是线程);

​ 锁实现的是不同线程间的数据共享

二、应用场景

一个我们接触多,容易理解,最可能用到的场景:

定义一个成员变量ThreadLocal,方法间调用不再传递业务参数,直接把参数set()到ThreadLocal里,到后面无论哪个方法里用到了可以直接get()获取.比如我们想统计各个接口的处理时间,把最开始的时间戳set到hreadLocal里,后面每个方法结束都可以get出来计算,在重新set新值,这样不用把时间当作参数传递了。它维护的副本变量,每个线程都有属于自己的变量,所以不会存在并发数据安全性问题。

三、原理

了解它的实现原理首先要清楚几个类以及他们之间得关系

Thread:线程,我们接触最多,跑我们的业务逻辑

ThreadLocal:一个跟Thread同级别的类,但它的内部类ThreadLocalMap是Thread的成员变量,Thread Local是联系Thread和ThreadLocalMap的桥梁,维护的是数据应用和数据存储。

ThreadLocalMap:是ThreadLocal的静态内部类,一个Thread对应一个ThreadLocalMap,它是为该线程存储数据的一种数据结构。它底层是一个Entry类型的数组实现

Entry:是ThreadLocalMap的内部类,他是由一组键值对组成,key就是ThreadLocal,它是一种弱引用,容易被回收;value就是我们要真正存储的数据。

他们之间的关系

一个Thread类对应一个ThreadLocalMap,ThreadLocalMap存数据是用Entry类型的数组实现的,Entry的键是ThreadLocal,值是要存的数据。所以一个ThreadLocalMap里可以存放多个ThreadLocal,每一个ThreadLocal对应该线程下的一种要存储的数据类型(如:int、String等)。

可以这么理解:Thread与ThreadLocalMap是一对一关系,他们与ThreadLocal是一对多关系.。

理解上面的关系后原理就很好总结了:ThreadLocal底层是一个类似于map的内部类ThreadLocalMap实现的,真正存储结构的是ThreadLocalMap,并且它与线程紧密关联,ThreadLocal作为一个桥梁维护Thread和ThreadLocalMap,负责操作ThreadLocalMap。ThreadLocalMap底层是Entry实现的,内部是一个Entry数组,这个Entry是由key和value组成,key就是ThreadLocal,value可以是任意对象,它继承了WeakReference,是个弱引用,所以key容易被回收,造成内存泄漏。

四、内存泄漏

ThreadLoca使用不当容易引起内存泄漏。何谓内存泄漏?Entry是键值对的形式存在,它继承了WeakRefencer<ThreadLocal<?>>是一种弱引用,它的key容易被GC回收,但值value是一直存在的,没有被回收,这就导致可能存在key是null,value有值的情况,当大量这种对象存在时,内存容易被占满。

解决方法:我们在使用完ThreadLocal后手动remove()一下,清空无效的value值

JDK的解决方案:在调set()、get()方法时内部都会遍历查找无效的value进行清空

五、用法

开发主要使用到的三个方法set()、get()、remove()

public class ThreadLocalTest {
    static ThreadLocal<Long> threadLocalLong = new ThreadLocal<Long>();
    static ThreadLocal<String> threadLocalString = new ThreadLocal<String>();
    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            final int sum=i;
            new Thread(()->{
                long l = System.currentTimeMillis();
                threadLocalLong.set(l);
                threadLocalString.set(Thread.currentThread().getName());
                System.out.println(threadLocalString.get()+"=初次set值="+ threadLocalLong.get());
                sleep(sum);
                System.out.println(threadLocalString.get()+"=再次set的值="+ threadLocalLong.get()+"=两次差值="+(threadLocalLong.get()-l));
                threadLocalLong.remove();
                threadLocalString.remove();
            },"线程-"+i).start();
        }
    }
    public static void sleep(int i) {
        try {
            Thread.sleep(3000*i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadLocalLong.set(System.currentTimeMillis());
    }
}

六、源码

源码重点关注set就可以,get、remove用到的都在set里有

public void set(T value) {
    Thread t = Thread.currentThread();
    //拿到只属于这个线程的私有变量map,说是map其实就是一个entry,里面就是我们要放的数据,类似于数组
    //map里面可能存在多种类型的数据,但这些数据都是属于这个线程的。对应使用就是定义多个ThrealLocal,但map就一个
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null){
        map.set(this, value);
    }else{
       //map为null,构建一个新得map,指向当前线程的ThreadLocal引用,
      //这个线程就有了自己专属的ThradLocalMap,并且这个map里存放了要set的值,这个值还以弱引用形式引用着这个线程
        t.threadLocals = new  ThreadLocal.ThreadLocalMap(this, value);
    }
}

getMap()没啥好说的,就是拿这个线程的ThreadLocalMap,重点在map.set()里,map.set(this, value)的实现

/**
 * 主要是在Entry数组里循环找当前线程的Entry,找不到就创建一个,遍历过程中顺便查了下Entry的弱引用(当前线程的引用对象)有没有被回收
 * 注意:
 *      1、Entry数组里是该线程下的任意类型数据,对应我们new的ThreadLocL
 *      2、内存泄漏:因为Entry是弱引用,如果应用被回收了,value也要清理,不然越堆越多,容易报内存泄漏
 *      3、前面已经判断了map是否为null,为啥后面还可能为null?很可能是正好触发了垃圾回收
 *      4、多个线程创建Entry往数组里放时,可能会触发数组的阀值,需要扩容
 *
 */
private void set(ThreadLocal<?> key, Object value) {
    //根据ThreadLocal的hashcode找到这个key可能所在的位置
    ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //从这个数组里去找属于当前线程的Entery
    for (ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
         ThreadLocal<?> k = e.get();
        if (k == key) {//如果是当前线程就给它赋值
            e.value = value;
            return;
        }
        //如果key是空了,i这个位置上已经没引用了,被GC回收了,
        if (k == null) {
        // 把key=null的Entry和当前key的Entry替换,并检查和清理其他空的Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果这个数组里还没有Entry,是一个空数组,则构建一个Entry放入数组
    tab[i] = new  ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    //如果数组容量达到阀值就要扩容了
    if (!cleanSomeSlots(i, sz) && sz >= threshold){
        rehash();
    }
}
private void replaceStaleEntry( ThreadLocal<?> key, Object value,int staleSlot) {
     ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
     ThreadLocal.ThreadLocalMap.Entry e;
    int slotToExpunge = staleSlot;
    //从当前位置往前面找,看前面位置上有没有引用为null的,直到找到数组里第一个引用为null的位置
    for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null; i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }
    //从当前位置往后面找,
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
         ThreadLocal<?> k = e.get();
         //如果存在key是当前线程的Entry,就赋值
        if (k == key) {
            e.value = value;
            //引用为null的Entry和当前线程的Entry交换位置(当前线程的Entry放到当前这个引用为null的位置,引用为null的Entry放到这个线程的位置上)
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            //经过上述操作后i位置上Entry引用为null
            if (slotToExpunge == staleSlot) {
                //如果前面位置上没有null引用(没走第一个循环)把这个空位置给slotToExpunge
                slotToExpunge = i;
            }
            //slotToExpunge值最终是一个null引用的Entry位置,结合下面的判断并且值只能是第一个null引用的位置
            //清除引用为null的Entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }
    //再次都找一下,都找完了也没找到当前线程的引用位置,就新构建一个
    tab[staleSlot].value = null;
    tab[staleSlot] = new  ThreadLocal.ThreadLocalMap.Entry(key, value);
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

七、总结

本文主要讲了ThreadLocal是什么、应用场景以及原理、源码等。作为入门级了解,知道是什么?用在哪?怎么用就可以,深入了解(面试扯皮)重点关注原理、内存泄漏、源码实现部分。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值