ThreadLocal

先来看看Jdk1.8对ThreadLocal的解释:

JDK1.8定义:当前类提供线程本地变量。这些变量不同于正常变量,而是访问这些变量的每一个线程,都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal通常是类中private static字段,期望与线程的某一个状态相关联(例如用户ID或事务ID)。

简单的来说,ThreadLocal是线程的本地变量,不用线程访问同一个ThreadLocal,得到的值是不同的。ThreadLocal保证了各个线程的数据互不打扰。

一个简单Demo
public class ThraedLocalDemo {
    private static ThreadLocal local = new ThreadLocal();

    public static void main(String[] args) {
        local.set("test");
        System.out.println(Thread.currentThread().getName()+"::"+local.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"::"+local.get());
            }
        }).start();
    }

}

//运行结果
main::test
Thread-0::null

当我们在main线程中进行set(),get()时可以获取到期望值。当我们在另外一个线程中尝试获取ThreadLocal中的值,得到的是null。

ThreadLocal源码

直接看get(),set()操作在源码中如何实现。

    public T get() {
        Thread t = Thread.currentThread();	//得到当前线程
        ThreadLocalMap map = getMap(t);		//从当前线程中获取ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);	//以当前对象为key,获取entry
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;	//得到value
                return result;
            }
        }
        return setInitialValue();	//初始化方法。源码很简单,需要详细了解可以查看源码
    }

    public void set(T value) {
        Thread t = Thread.currentThread();		//得到当前线程
        ThreadLocalMap map = getMap(t);			//得到当前线程的ThreadLocalMap对象
        if (map != null)
            map.set(this, value);				以当前对象为key,写入map中
        else
            createMap(t, value);
    }

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

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

从上面源码可以看出,无论 get() 或者 set() 方法,操作的都是当前线程 Thread中的ThreadLocalMap对象。

public class Thread implements Runnable {
	...
    ThreadLocal.ThreadLocalMap threadLocals = null;	//在Thread类中,维护的threadLocals
    ...
}
ThreadLocalMap

JDK1.8定义:ThreadLocalMap是一个定制的哈希映射,仅适用于维护线程本地值。ThreadLocalMap类是包私有的,允许在Thread类中声明字段。为了帮助处理非常大且长时间的使用,哈希表entry使用了对键的弱引用。有助于GC回收。

ThreadLocalMap定义在ThreadLocal中的静态内部类,内部维护了一个Entry类和Entry[]。Entry的key是当前ThreadLocal对象,value为ThreadLocal对象所对应的值。Entry[]存放当前线程中所有ThreadLocal变量。ThreadLocalMap内部实现如下图:

img

所以,我们继续了解ThreadLocalMap代码是怎么实现的。

/*
	ThreadLocalMap中除了主要的增删改查等操作外,
	还有扩容等操作,因为原理和HashMap扩容大同小异,
	需要的同学去查看源码吧,原谅我犯懒没有写。
*/
static class ThreadLocalMap {

    /*
        Entry并不同于HashMap,内部没有next指针指向下一个Entry,
        也就是当发生Hash冲突时,不是以链表的形式解决冲突。
    */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    private Entry[] table;		//存储所有ThreadLocal和value的Entry

    private int size = 0;

    private void set(ThreadLocal<?> key, Object value) {

        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);	//根据key的hashCode和长度得到索引位置
		/*
		从索引处开始遍历数组中所有Entry
        依次判断当前位置的Entry.key是不是当前ThreadLocal,满足条件则进行覆盖,结束;
        如果当前位置为空,则使用新的key,value进行覆盖,同时清理历史key=null的陈旧数据,结束;
        否则将索引指向下一个位置,继续遍历。
        同样的,在执行get(),remove()时也会使用这种方式进行遍历寻找。
        */
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            //如果当前ThreadLocal对象==e.key,覆盖原有值
            if (k == key) {		
                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();
    }

}

从上面代码可以发现,使用hashCode确定索引时,如果冲突严重时,会多次遍历整个数组,效率很低。

内存泄露
为什么ThreadLocal可能会导致内存泄露。

从上面代码可以知道,Entry类继承了WeakReference,value是以强引用的形式保存在Entry中,但是ThreadLocal是以弱引用的形式保存在WeakReference中。

这就导致了一个问题,ThreadLocal没有外部强引用时,发生GC时会被回收。如果创建的ThreadLocal一直运行下去,那么Entry中的value就不会被回收,发生内存泄露。

如何避免内存泄露

在调用ThreadLocal.get(),set()方法时,可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的Value就没有被引用,会被GC回收。当然,如果直接调用remove()方法,肯定会删除对应的Entry对象。

所以,要养成良好的编码习惯,使用完ThreadLocal之后,进行remove()操作。

ThreadLocal<String> tl = new ThreadLocal();
try {
    localName.set("value");
    // 其它业务逻辑
} finally {
    localName.remove();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值