ThreadLocal提供线程的局部变量,这种变量与普通变量的区别在于,每个访问这种变量的线程都有自己的、独立的变量副本。用于解决多线程间的数据隔离问题。
使用场景其实ThreadLocal在很多开源框架中都有应用:
Spring中的事务管理器,比如TransactionSynchronizationManager等。Mybatis中的ErrorContext类,使用ThreadLocal实现线程安全的单例。存储session中的一些参数,比如用户信息等。APIThreadLocal提供了4个常用方法:
set()方法,设置当前线程中变量的副本。get()方法,获取 ThreadLocal在当前线程中保存的变量副本。remove()方法,清空当前线程中变量的副本。initialValue()是一个 protected方法,一般是用来重写的,如果在没有set的时候就调用 get,会调用 initialValue方法初始化内容。private static ThreadLocal SimpleDateFormat simpleDateFormatThreadLocal = new ThreadLocal SimpleDateFormat (){ //重写此方法,初始化ThreadLocal的value @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); };原理
那么怎么实现数据隔离的,我们从源码的角度进行分析。
我们先看ThreadLocal类的get()方法。
public T get() { Thread t = Thread.currentThread(); //通过当前线程获取ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; return 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; //返回Thread实例的成员变量threadLocals ThreadLocalMap getMap(Thread t) { return t.threadLocals; //给Thread实例的成员变量threadLocals赋值 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
从源码可以看出,数据是存在于Thread类的成员变量threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
上面写了一段注释,翻译过来就是,关于该线程的ThreadLocal的值,由ThreadLocal类进行维护。
所以很清楚了,数据隔离的实现是因为ThreadLocal类操作的是Thread的成员变量threadLocals。每个线程Thread都有自己的threadLocals,从而互相不影响。
threadLocals这个成员变量的本质又是ThreadLocalMap类,它是ThreadLocal的内部类,下面我们研究一下这个内部类的数据结构。
数据结构先看一下源码:
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; //初始化容量 private static final int INITIAL_CAPACITY = 16; //散列表 private Entry[] table; //有效数量 private int size = 0; //负载因子 private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; //构造器 ThreadLocalMap(ThreadLocal ? firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
这一看跟HashMap还有几分相似,但是哈希冲突的处理方式,ThreadLocalMap采用的是开放寻址法(自行百度一下,这里不多解释了),大概长这个样子:
所以这里可以看出ThreadLocal的引用可以定位到ThreadLocalMap里散列表table[]里的值。
内存泄漏问题我们从源码中可以看到Entry是继承WeakReference类,key是弱引用,value是强引用。为什么要设计成弱引用?不如反过来想,如果设置成强引用会有什么效果。
如果Entry对象的Key每个都强引用到ThreadLocal对象的话,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,ThreadLocalMap才会跟着回收。
当作为Key的ThreadLocal对象设置成弱引用对象后,在系统GC的时候,ThreadLocal对象就会被回收。
但是这样就能防止内存泄漏吗?
其实不然!因为Value还是强引用对象,当Key被回收后,key变成了null值,而Value依然存在一条强引用链:Thread Ref - Thread - ThreaLocalMap - Entry - value永远无法回收,而这块value也永远不会被访问到了,最终造成内存泄漏。
所以在设计ThreadLocalMap时就考虑到这个问题,在ThreadLocal的get()、set()、remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
总结其实ThreadLocal并没有解决多线程间数据共享的问题,而是使数据在不同线程有不同的副本,那么就不需要解决共享数据的问题。每个线程持有一个ThreadLocalMap对象,该ThreadLocalMap对象只会被持有它的线程访问,所以不存在线程安全问题。ThreadLocalMap的数据结构类似HashMap,里面由Entry[]数组、size、负载因子等组成,采用开放寻址法解决哈希冲突。ThreadLocalMap的Entry对ThreadLocal对象是弱引用,GC回收后,会产生一些key为null的value无法被访问,也无法被回收,最终导致内存泄漏。预防措施是调用ThreadLocal的remove()方法,清除掉ThreadLocalMap里面key为null的value。非常感谢你的阅读,希望这篇文章能给到你帮助和启发。
觉得有用就点个赞吧,你的点赞是我创作的最大动力~
我是一个努力让大家记住的程序员。我们下期再见!!!
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!编者按:本文源自阿里云云效团队出品的《阿里巴巴DevOps实践指南》,扫描上方二...
伴随着互联网技术的不断发展,网络安全攻击事件也层出不穷,市面上大多数网站都...
服务器租用 对现在的年轻人来说并不陌生,他们在游戏的过程中或多或少的都会接触...
大数据是软件即服务(SaaS)业务模型的关键。大数据创新者Panoply曾经对一种名为数...
怎么买 域名 自己做网站?买域名是自己做网站的第一步,国内域名购买平台有很多...
【51CTO.com快译】随着新技术的出现、经济不确定性以及不断增加的法规对消费电子...
网站 服务器租用 如何选择适合自己的 网站搭建完成后,我们需要根据网站的具体情...
之前有个朋友提到了抖音数据的获...
云点播支持多种方式,播放上传及转码后的视频。其中,播放主要分为以下三类场景...
很多游戏都会有攻击,因为竞争太激烈,这些都很正常,所以大家找真正抗攻击的高...