从源码看Thread&ThreadLocal&ThreadLocalMap的关系与原理
1.三者的之间的关系
ThreadLocalMap是Thread类的成员变量threadLocals,一个线程拥有一个ThreadLocalMap,一个ThreadLocalMap可以有多个ThreadLocal。
ThreadLocalMap是ThreadLocal的内部类,ThreadLocal的set(),get(),remove()方法其实都是对ThreadLocalMap的操作。ThreadLocalMap中是以内部类Entry的形式关联ThreadLocal和对应的Value,其中Entry对ThreadLocal为弱引用(WeakReference<>).
如下图,大概描述了下三者的关系
文章图片
2: 结构分析 首先看下Thread类,可以看到有个ThreadLocalMap类型的成员变量threadLocals,之后所有针对当前线程的ThreadLocal的存取,都是该变量来操作。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
}
再来看下ThreadLocalMap的结构,它是ThreadLocal的内部类
static class ThreadLocalMap {
//内部类Entry继承了弱引用() static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */
//通过ThreLocal.set()保存的值 Object value;
//构造函数 Entry(ThreadLocal> k, Object v) {
//调用WeakReference的构造方法,实现Entry对ThreadLocal的弱引用 super(k); value = https://www.it610.com/article/v; } }/** * 初始容量,即table的初始化大小 */ private static final int INITIAL_CAPACITY = 16; /** * Entry数组,用来保存每一个ThreadLocal */ private Entry[] table; /** * 当前table中实际存放的Entry的数量 */ private int size = 0; /** * 扩容阈值,默认为0 */ private int threshold; // Default to 0/** * Set the resize threshold to maintain at worst a 2/3 load factor.
设置扩容阈值的方法,可以看到ThreadLocalMap中的扩容的负载因子为2/3 */ private void setThreshold(int len) { threshold = len * 2 / 3; }
3.完整流程分析 正常情况下我们使用ThreadLocal来存取变量都是这样的
ThreadLocal test = new ThreadLocal<>(); test.set("111");
首先看下ThreadLocal.set(T value)方法
public void set(T value) {
//获取当前线程 Thread t = Thread.currentThread();
//根据线程获取ThreLocalMap,其实就是获取Thread的成员变量 ThreadLocalMap map = getMap(t);
//如果map!=null,则则将当前ThreadLocal进行设置 if (map != null) map.set(this, value); else
//map==null,则对该线程的ThreadLocalMap进行初始化 createMap(t, value); }
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
当前线程第一次使用ThreadLocal, createMap()方法初始化ThreadLocalMap
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//初始化table,初始化大小为16 table = new Entry[INITIAL_CAPACITY];
//计算插入的数组下标,将threadLocael的hashcode与15进行按位异或操作 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将新构建的Entry放到计算的数组下标上 table[i] = new Entry(firstKey, firstValue);
//table中实际长度赋值1 size = 1;
//设置扩容阈值,这个方法我们上面看到过,内部算法就是initial_capacity * 2/3 setThreshold(INITIAL_CAPACITY); }
ThreadLocalMap已经存在,再次添加ThreadLocal
private void set(ThreadLocal> key, Object value) { //获取当前table Entry[] tab = table; int len = tab.length;
//计算出数组插入下标 int i = key.threadLocalHashCode & (len-1); //从计算出的下标位置i开始遍历table数组,直到下一个元素Entry为null时停止
//这里解决Hash冲突的方法采用的线性探测法,计算出的位置有值的话就相邻的向下一直探索直到有位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); //判断当前遍历的ThreadLocale是否和添加进来的key相等 if (k == key) {
//更新value e.value = https://www.it610.com/article/value; return; } //如果存在Entry中ThreadLocal为null的情况,即该线程变量已过时,则对过时的Entry进行清除 if (k == null) { replaceStaleEntry(key, value, i); return; } } //走到这里说明目前的table中不存在该ThreadLocale,则创建新Entry放到计算的下标处 tab[i] = new Entry(key, value);
//table实际长度+1 int sz = ++size;
//if(!快速遍历一遍table判断是否存在Entry中ThreadLocal为null的情况&&当前table的实际长度>=扩容阈值) 则进行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
看完了ThreadLocal的set()方法,再来看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")
//返回的Entry!=null的话直接返回其储存的value值 T result = (T)e.value; return result; } }
//如果ThreadLocalMap==null或者找不到该Entry,返回设置的默认值 return setInitialValue(); }
ThreadLocalMap!=null时调用getEntry()方法
private Entry getEntry(ThreadLocal> key) {
//计算出在table中的数组下标 int i = key.threadLocalHashCode & (table.length - 1);
//获取指定下标中的E Entry e = table[i];
//如果Entry!=&&ThreadLocal==当前的ThreadLocale,直接返回该Entry if (e != null && e.get() == key) return e; else
//找到的元素不对或者位置上没有元素 return getEntryAfterMiss(key, i, e); }private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
//获取当前table Entry[] tab = table;
//获取table长度 int len = tab.length; while (e != null) {
//如果Entry!=null,就取出来再判断一下ThreadLocal是否相同 ThreadLocal> k = e.get(); if (k == key) return e; if (k == null)
//清除掉key已失效的E expungeStaleEntry(i); else //以当前的数组下标下后遍历Entry,因为set()时插入Entry发生Hash冲突时用的是线性探测法解决的,所以get()查找时也按此原则
i = nextIndex(i, len); e = tab[i]; }
//如果遍历完table都找不到,返回null return null; }
get()获取时ThreadLocalMap还为空时调用的初始化方法setInitialValue()方法
private T setInitialValue() {
//获取初始化value,该方法内部直接返回的为null T value = https://www.it610.com/article/initialValue();
//获取当前线程 Thread t = Thread.currentThread();
//获取该线程的ThreadLocalMap ThreadLocalMap map = getMap(t);
//如果map!=null,则用初始化的值来添加 if (map != null) map.set(this, value); else
//如果map==null,则用这个初始化值null和当前的这个ThreadLocal来创建ThreadLocalMap进行初始化
createMap(t, value); return value; }
使用完ThreadLocal,最好清除下remove()
public void remove() {
//获取当前线程的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null)
map!=null就进行删除 m.remove(this); }private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
//获取到下标后线性探测法遍历table,找到后进行删除 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
remove()调用的关联方法:
public void clear() {
//将Entry内部的弱引用的ThreadLocal置为null,方便下一次GC时进行对ThreadLocal对象进行回收 this.referent = null; }
//释放table中的Entry private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //将Entry的value的引用置为null,此时Entry不再持有任何引用,ThreadLocal和value的引用都已清除, // expunge entry at staleSlot tab[staleSlot].value = https://www.it610.com/article/null;
//将该位置的Entry的引用置为null,此时此Entry也不再被table强引用,下次GC时也会回收 tab[staleSlot] = null;
//table实际长度-1 size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == null) { e.value = https://www.it610.com/article/null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
4.ThreadLocal内存泄漏问题分析 通过上述的源码我们ThreadLocal的使用及原理有了大致的了解,那么在使用ThreadLocal的同时很大可能会出现内存泄漏问题,下面我们来探讨下这究竟是怎么回事,图来源于网络
文章图片
当一个Thread使用完ThreadLocal存储变量完,对应的ThreadLocal的引用被清除,这时候该ThreadLocal的强引用被清除,但是Thread的ThreadLocalMap中的Entry的key还存在着ThreadLocal的弱引用,当发生Young GC时该弱引用就会被清除,这时就会存在Entry中key=null,这导致该ThreadLocalMap永远访问不到该value,value就会内存泄漏,除非ThreadLocalMap对象也被清除。
这是由于Threrd和ThreadLocalMap的生命周期一样长,如果该在ThreadLocal清除后该Thread一直存活,那么就一直存在着value内存泄漏的问题。
既然使用了对ThreadLocal的弱引用出现了Entry中value的内存泄露,那为什么还要使用弱引用呢?如果变成强引用呢?
我们来看下,如果Entry中变成强引用ThreadLocal, 当外部的ThreadLocal强引用被清除后,由于Entry内部还有强引用,但外部又无法再通过ThreadLocal访问到,就会导致Entry的内存泄漏,泄漏对象变的更大,并且GC回收时也不会回收该Entry对象。
针对该内存泄漏现象,官方也做了相应的处理,我们在上面的源码中可以看到,不管是在调用ThreadLocal的set(),get()还是remove()方法每次在调用时遍历table的时候会因为hash冲突向下遍历一段距离,这遍历过程中如果有发现Entry中ThreadLocal为null的情况,会进行处理,将Entry完全清除掉,但是这个遍历的范围非常有限,很有可能遍历不到为null的那个Entry,即使set()方法在第一次插入ThreadLocal时还会进行一次快速的遍历table,但终究不是完全遍历,所以通过官方的优化,内存泄漏的问题还是不能够很好的解决。
内存泄漏的问题我们使用规范的话,完全是可以避免的:
1.在每次使用完ThreadLocal时,使用ThreadLocal.remove()方法,这样就会清除调Entry中的key和value的引用。
2.将ThreadLocal对象设置为private static 变成共享对象,让所有线程都使用该ThreadLocal对象,这样ThreadLocal就一直存在外部强引用,GC时就不会清除Entry的ThreadLocal,不出出现内存泄漏,但是加大了内存开销,尽量还是使用完就使用remove()进行处理。
另外一提:
因为线程池中的线程会存在复用,所以可以能存在读出脏数据的问题。即当线程池中某个线程使用ThreadLocal存储数据时,使用过后没有remove,等下次从线程池调用到该线程的时候,就会读到该线程上一次执行任务时的数据。所以务必需要remove()。
【从源码看Thread&ThreadLocal&ThreadLocalMap的关系与原理】ps: 由于笔者水平有限,可能存在一些地方理解不正确,希望大家能够指出。
推荐阅读
- 一个小故事,我的思考。
- Docker应用:容器间通信与Mariadb数据库主从复制
- 第三节|第三节 快乐和幸福(12)
- 开学第一天(下)
- 一个人的碎碎念
- 死结。
- 我从来不做坏事
- “成长”读书社群招募
- 拍照一年啦,如果你想了解我,那就请先看看这篇文章
- 危险也是机会