ThreadLocal,也就是线程本地变量。如果你创建了一个 ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
threadLocal内存结构图
public void set(T value) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);
}
从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。
如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value
key 的赋值,使用的是 WeakReference 的赋值。
1.Thread类有一个类型为 ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的 ThreadLocalMap。
2.ThreadLocalMap内部维护着 Entry数组,每个 Entry代表一个完整的对象,key是 ThreadLocal的弱引用,value是 ThreadLocal的泛型值。
3.每个线程在往 ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个 ThreadLocal作为引用,在自己的 map里找对应的 key,从而实现了线程隔离。
4.ThreadLocal本身不存储值,它只是作为一个 key来让线程往ThreadLocalMap里存取值。
threadLocal内存分配
为什么会内存泄漏?
ThreadLocalMap中使用的 key为 ThreadLocal的弱引用。“弱引用:只要垃圾回收机制一运行,不管 JVM的内存空间是否充足,都会回收该对象占用的内存。”那么现在问题就来了,弱引用很容易被回收,如果 ThreadLocal(ThreadLocalMap的 Key)被垃圾回收器回收了,但是 ThreadLocalMap生命周期和 Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的 key没了,value还在,这就会造成了内存泄漏问题。
如何解决内存泄漏?
很简单,使用完 ThreadLocal后,及时调用 remove()方法释放内存空间。
为什么还要设计为弱引用呢?
key设计成弱引用同样是为了防止内存泄漏。假如 key被设计成强引用,如果 ThreadLocal Reference被销毁,此时它指向 ThreadLocal的强引用就没有了,但是此时 key还强引用指向 ThreadLocal,就会导致 ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.
我们可能都知道 HashMap使用了链表来解决冲突,也就是所谓的链地址法。ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。
如上图所示,如果我们插入一个 value=27的数据,通过 hash计算后应该落入第 4个槽位中,而槽位 4已经有了 Entry数据,而且 Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。
在 get的时候,也会根据 ThreadLocal对象的 hash值,定位到 table中的位置,然后判断该槽位 Entry对象中的 key是否和 get的 key一致,如果不一致,就判断下一个位置。
ThreadLocalMap扩容的两个条件:
1.在 ThreadLocalMap.set() 方法的最后,如果执行完 启发式清理(源码中:cleanSomeSlots() 方法 ) 工作后,未清理到任何数据 && 当前散列数组中 Entry 的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:
2.rehash() 中,先进行一轮 探测式清理(源码中:expungeStaleEntry() 方法) 流程,然后判断size >= threshold - threshold / 4 也就是size >= threshold * 3/4 来决定是否扩容
每次扩容为原来数组大小的2倍(ThreadLocalMap初始容量为16,必须是2的次幂)
resize()
扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的 entry 为 null 的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值
1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅
如:存储用户session,数据库连接处理数据库事务,数据跨层传递(controller,service, dao),Spring使用ThreadLocal解决线程安全问题