ThreadLocal学习笔记

本文深入解析了ThreadLocal的工作原理,包括其如何通过ThreadLocalMap实现线程间的数据隔离,并探讨了使用过程中需要注意的问题,如内存泄漏及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先来看一下Lock和ThreadLocal的区别,Lock是用来协调多个线程之间数据共享的,ThreadLocal是用来解决多个线程之间数据隔离的。

下面就ThreadLocal做一下具体的分析,看它是如何做到线程之间数据隔离的,先来看它的set方法源码:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

   通过源码我们可以看到每个线程都有一个类型为ThreadLocalMap的变量threadLocals,每个线程隔离的数据最后都保存在这个变量里面。这个map的key就是一个ThreadLocal对象,value就是线程隔离的数据。 由此可见ThreadLocal是通过ThreadLocalMap来实现线程数据隔离的。 

   下面再看一下ThreadLocal的get方法源代码:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }


   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

   通过源码我们可以看到每次取值的时候,先从当前线程中获取对应的ThreadLocalMap变量,然后从变量中根据key(记得这里是ThreadLocal对象--this)获取对应的值(如果没有会直接返回初始值--setInitialValue)。在这里其实也可以将当前线程作为ThreadLocalMap变量的key,也能达到线程隔离,但是却只能实现一个变量的隔离,ThreadLocal作为key可以实现多个变量的隔离,每个变量对应一个ThreadLocal对象。至此,我们可以清晰的看到ThreadLocal是如何实现线程数据隔离的了。

   但是我们还是有几个问题需要注意:

   1)如果存在线程重用或是长时间存在的情况,则一定要将ThreadLocal声明为static,否则可能存在内存泄露,如果没有重用也可以不声明,但是声明也没有副作用,所以结论是使用ThreadLocal是一定要声明为static。

   2)当ThreadLocal不使用时,一定要调用remove方法,调用set(null)方法还不够,在极端情况下会造成内存泄露。为什么呢?我们知道调用set(null)方法只会将ThreadLocal对应的值设置为null,但是ThreadLocal本身还是被线程变量threadLocals所引用的,导致垃圾回收时不会回收ThreadLocal对象,即便是该对象已不再使用。当调用remove方法时,会将当前的ThreadLocal对象从对应的线程的threadLocals里面移除,该ThreadLocal对象这个时候只被ThreadLocalMap.Entry对象引用,而该对象是继承自WeakReference,里面是通过弱引用指向ThreadLocal对象的,所以在垃圾回收时会回收ThreadLocal对象。具体的源代码如下:

ThreadLocal类的remove方法

      public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap 的remove方法

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

e.clear方法

    public void clear() {
this.referent = null;
    }

expungeStaleEntry方法(只取了一小段说明问题)

    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;


            // expunge entry at staleSlot
            tab[staleSlot].value = null;   
            tab[staleSlot] = null;
            size--;

            //这里省略了一大段无关代码

    }

ThreadLocal执行remove方法的示意图如下:


一、当ThreadLocal没有定义为static且存在线程重用或是线程长时间存在时,系统中的ThreadLocal运行状况如下图。当创建业务对象被回收后ThreadLocal还是被ThreadLocalMap引用,不会被回收,所以长时间运行后ThreadLocal对象将会很多,也就是说ThreadLocalMap里面存放了太多的ThreadLocal对象,存在内存泄露的情况。


二、当ThreadLocal定义为static,即便是存在线程重用或是线程长时间存在的情况,从图中可以看出都是引用一个ThreadLocal对象。当创建业务对象被回收后ThreadLocal虽然还是被ThreadLocalMap引用,不会被回收,但是只会有一个ThreadLocal对象,不会像上图那样ThreadLocal对象会随着运行时间的增加而增加,一般不会存在内存泄露。


三、当ThreadLocal被声明为static且线程隔离的数据对象存在多个时,系统中ThreadLocal的运行情况如下图,每种不同颜色代表一种需要线程隔离的数据对象:




备注:如果ThreadLocal不设置为static,要防止内存泄露是否有其它办法,其实如果ThreadLocal的

   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

修改为

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(new WeakReference(this), firstValue);
    }

应该也可以解决。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值