ThreadLocal讲解

最近面试会被经常问到ThreadLocal的作用和原理,所以这里有必要学习一下。

什么是ThreadLocal

ThreadLoal 被称为线程局部变量,即该变量运行在线程中时,每个线程都独立拥有它而不和其他线程中的这个值相冲突,其目的就使这个变量只属于当前线程,和其他线程无关。ThreadLoal 解决的是变量在不同线程间的隔离性,也就是说不同线程拥有自己的值。

 

ThreadLocal实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

一个线程内可以存在多个 ThreadLocal 对象,因为在 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 内部定义的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

首先我们先看 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();  
} 

首先取得当前线程,然后通过getMap(Thread t)方法获取到一个map,map的类型为ThreadLocalMap。

如果map不为空,接着获取ThreadLocalMap.Entry对象。ThreadLocalMap.Entry对象是以this指向的ThreadLocal对象为键在ThreadLocalMap中进行查找的。

如果获取到的ThreadLocalMap.Entry不为空,则返回value值,即和当前线程绑定的值。

如果map为空或者ThreadLocalMap.Entry为空,则调用setInitialValue()方法设置初始值。

接下来我们将上面的每一句来仔细分析:首先看一下getMap(Thread t)方法中做了什么:

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

在getMap(Thread t)中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。Thread类中的成员变量threadLocals是什么?源码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals是一个ThreadLocalMap,它是ThreadLocal类的一个静态内部类,ThreadLocalMap的实现如下:

static class ThreadLocalMap {
...
    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
...
}

可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键。

setInitialValue()的实现如下:

private T setInitialValue() {  
   T value = initialValue();  
   Thread t = Thread.currentThread();  
   ThreadLocalMap map = getMap(t);  
   if (map != null)  
       map.set(this, value);  
   else  
       createMap(t, value);  
   return value;  
} 

protected T initialValue() {
    return null;
}

先调用initialValue()方法获取初始值,默认返回null。然后获取和当前线程相关的ThreadLocalMap,如果map不为空,就设置键值对,如果map为空,调用createMap(Thread t, T firstValue),createMap(Thread t, T firstValue)的实现如下:

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

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,每个线程中都有一个独立的ThreadLocalMap,它所存储的值只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发问题。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

这个时候再看set(T value)方法就简单多了,源码如下:

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

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,其中以this指向的threadlocal对象为健值,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

总结一下:

  • 通过ThreadLocal设置的对象是存储在每个线程自己的ThreadLocalMap类型的成员变量threadLocals中的;
  • 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
  • 我们发现如果没有先set的话,即在map中查找不到对应的ThreadLocalMap.Entry,则会通过调用setInitialValue()方法,而在setInitialValue()方法中,有一条语句是T value = initialValue(), 而默认情况下,initialValue()方法返回的是null。所以如果想在get之前不需要调用set就能返回值的话,必须重写initialValue()方法。

 

内存泄漏问题

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以 ThreadLocal 如果没有被外部强引用的情况下,会在垃圾回收的时候被清理掉,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

所以最佳实践,应该在我们不使用的时候,主动调用remove方法进行清理。

阿里开发规范建议如下:

这里把ThreadLocal定义为static还有一个好处就是,由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除!!!

 

ThreadLocal用在什么地方

讨论ThreadLocal用在什么地方前,我们先明确下,如果仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的。

ThreadLocal归纳下来就2类用途:

  • 保存线程上下文信息,在任意需要的地方可以获取。
  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失。

保存线程上下文信息,在任意需要的地方可以获取

由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

比如每个请求怎么把一串后续操作关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。

还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失

由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全。ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。这类场景阿里规范里面也提到了:

但是ThreadLocal也有局限性,我们来看看阿里规范:

每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的,所以ThreadLocal无法解决共享对象的更新问题!

 

参考:

https://blog.csdn.net/dakaniu/article/details/80829079

https://blog.csdn.net/lirenzuo/article/details/92821256

https://www.cnblogs.com/luxiaoxun/p/8744826.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值