thread 源码_Thread是什么

(63) 2024-07-25 07:01:01

ThreadLocal,可以理解为线程局部变量。同一份变量在每一个线程中都保存一份副本,线程对该副本的操作对其他线程完全是不可见的,是封闭的。

一、ThreadLocal简单示例

public class Main { private static ThreadLocal<Integer> tl = new ThreadLocal<>(); public static void main(String[] args) { tl.set(1); Thread t = new Thread(() -> { tl.set(2); System.out.println("子线程:" + tl.get()); }); t.start(); System.out.println("主线程:" + tl.get()); } }

最终的输出如下:

thread 源码_Thread是什么 (https://mushiming.com/)  第1张

 可以看出,各个线程内的ThreadLocal互不干扰,每个线程也只能访问自己独有的ThreadLocal变量。

那么ThreadLocal的结构是怎么样的呢?


二、ThreadLocal的结构

Thread、ThreadLocal与ThreadLocalMap的关系图如下:

thread 源码_Thread是什么 (https://mushiming.com/)  第2张

 从上面的结构图我们可以看出:

(1)每个Thread内部都有一个ThreadLocalMap,可以理解为简单版的HashMap。

(2)map的key是ThreadLocal类型的,而value的类型则是ThreadLocal的泛型类型。在本例中,value是Intege类型的。

在我刚学ThreadLocal的时候,我觉得他应该是这样设计的:

ThreadLocal里面有一个map容器,key是线程id或线程名称,value是副本的值,简单又好理解。那为什么jdk不这样设计呢(当然早期就是这样设计的)?

在jdk8中,map被放入到了Thread中,ThreadLocal更像是一个工具类。

那么,jdk8这样设计的好处是什么呢?

(1)如果map被放到ThreadLocal中,那么map的大小取决于线程数量。当线程数特别多的时候,势必会影响到map的查找、插入与扩容的效率。而在jdk8中,map的大小取决于ThreadLocal的数量,这个数量是可控的,一般不可能声明出那么多的ThreadLocal。

(2)在早期的设计中,当线程消亡时,需要在每一个关联的ThreadLocal的map中做一些清理工作,比较麻烦。而在jdk8中,线程消亡时,内部的map容器也随之消亡。


三、ThreadLocal有哪些方法

先从比较简单的set与get方法说起

有关ThreadLocalMap的方法,我们将会在下个章节进行梳理。

set方法

 public void set(T value) { //获取当前的操作线程 Thread t = Thread.currentThread(); //获取线程内部的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //如果map不为空,则直接将(k:当前ThreadLocal实例,v:副本值)放入进map中 map.set(this, value); else //如果map为空,则创建该线程的ThreadLocalMap,并将(k,v)放入进map中 createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

可以看到,Thread内部的ThreadLocalMap是懒加载的,只有在第一次使用的时候,才会创建map。

get方法

 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //如果map不为空,则获取键为该ThreadLocal对象的Entry实例 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue() { //获取初始值,默认是null 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; } 

看的出,get方法同样会触发map的初始化。

remove方法

 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //最终还是调用ThreadLocalMap的方法,移除key为当前ThreadLocal的Entry m.remove(this); }

好家伙,ThreadLocal工具人的身份石锤了。

核心的代码都在ThreadLocalMap中,他是ThreadLocal内的一个静态内部类。


四、ThreadLocalMap探究

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没有直接使用HashMap,而是一个经过定制化的map。map中的每一项都是一个Entry,key是对ThreadLocal的一个弱引用(这个后面会再解释)。

成员变量

 //初始容量,必须是2的整数次方 private static final int INITIAL_CAPACITY = 16; //Entry数组。其长度也必须是2的整数次方 private Entry[] table; //数组中不为null的Entry个数 private int size = 0; //扩容阈值,当size≥threshold时,就会发生扩容。默认为0,会在构造方法中重新设置 private int threshold;

基本方法

 //设置扩容阈值为表长度的2/3 private void setThreshold(int len) { threshold = len * 2 / 3; } //下一个索引,当索引为len-1时,下一个索引为0 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //上一个索引,当索引为0时,上一个索引为len-1 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }

ThreadLocalMap不同于HashMap,HashMap使用链地址法解决冲突,而ThreadLocalMap使用线性探测法。即当前下标存在冲突时,检查下一个下标是否存在冲突,你可以把数组看成一个环。

构造方法

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化一个容量为16的table table = new Entry[INITIAL_CAPACITY]; //计算当前ThreadLocal的下标 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; //设置扩容阈值为16的2/3,即10 setThreshold(INITIAL_CAPACITY); }

在计算ThreadLocal的下标的时候,用到了

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

生成哈希值的时候,用到了以下代码:

 private final int threadLocalHashCode = nextHashCode(); //使用AtomicInteger类型是保证加法的原子操作 private static AtomicInteger nextHashCode = new AtomicInteger(); //该魔数使得在该table上散列均匀,这里不细究其原理 private static final int HASH_INCREMENT = 0x61c88647; //返回下一个哈希值,仅仅是在当前值的基础上再加上魔数 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

在计算下标的时候,使用到了& (INITIAL_CAPACITY - 1),这里和HashMap是一样的算法(有关HashMap的连环问,可以参考我的这篇文章HashMap夺命连环问)。

前面说过,Entry数组的容量必须是2的整数次方,那么在这样的前提下,hashCode%len是和hashCode&(len-1)相等的,而位运算更加的快速。

例如len=16,len-1的二进制为01111,即将最高位变为0,小于16的部分全为1。那么hashCode&(len-1)之后,hashCode中≥16的位全部被与为0,小于16的被保留了下来,从而达到对容量取余相同的效果。

getEntry方法

这个就是ThreadLocal.get方法调用的底层逻辑

 private Entry getEntry(ThreadLocal<?> key) { //计算下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //找到则返回 if (e != null && e.get() == key) return e; else //利用线性探测法继续寻找 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //只要Entry不为null,就一直寻找。如果为null,说明真的找不到了 while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //key为null,说明ThreadLocal已经被回收,那么回收其value expungeStaleEntry(i); else //寻找i的下一个下标 i = nextIndex(i, len); //将e设置为下一个Entry e = tab[i]; } return null; }

expungeStaleEntry方法

即清理那些key为null的Entry

 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //断开对value的强引用 tab[staleSlot].value = null; //断开对Entry的强引用 tab[staleSlot] = null; size--; Entry e; int i; //从staleSlot的下一个位置开始,直到遇到为null的Entry结束 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { //断开对value与Entry的强引用 e.value = null; tab[i] = null; size--; } else { //如果当前的Entry不为null,则进行重新散列 int h = k.threadLocalHashCode & (len - 1); if (h != i) { //如果重新散列后,位置发生变动 tab[i] = null; //一直找到一个空位置 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }

expungeStaleEntry方法有两个作用:

(1)从staleSlot位置开始,在遇到空Entry之前,清理当前位置的Entry

(2)如果当前Entry不为空,则进行重新散列。重新散列后的位置不为空Entry的话,则选择下一个下标。

明明清理空Entry就行了,为什么需要对非空Entry还要再做一次重新散列呢?
是为了下一次get的时候,避免遇到空Entry需要执行expungeStaleEntry方法。

expungeStaleEntry方法可以理解为清理某一段数组,遇到null就停下来了,并不是全量清理。

set方法

 private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //存在则更新 if (k == key) { e.value = value; return; } if (k == null) { //替换当前失效的Entry replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //当没清理到任何数据且size≥阈值的时候,进行扩容 rehash(); }

replaceStaleEntry与cleanSomeSlots方法,我们不继续深入了,两个方法的主要想法依然是去清理无效Entry,即key为null的Entry。

rehash方法

 private void rehash() { //该方法对于table上每一个Entry,都执行了expungeStaleEntry方法 //可以理解为整体清理 expungeStaleEntries(); //threshold - threshold / 4 =3/4*threshold //默认的threshold =2/3*len,因此只要size>=1/2*len,即占了一半之后,就考虑扩容 //为什么不按传统的size>=threshold来考虑扩容呢? //因为执行一次全部清理后,依然还占有一半容量,那么就说明冲突可能会趋于严重,不如早点执行扩容操作。 if (size >= threshold - threshold / 4) resize(); } //对每一个位置上都进行检查并清理 private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //扩容为原来的两倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; //将旧位置的Entry重新计算下标放入新table中 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }

可以看得出来,在ThreadLocal的get与set方法中,都会去检查Entry的key是否为null,如果为null的话,会进行一些局部的清理工作。

当需要进行扩容时,会进行一次整体清理。


五、InheritableThreadLocal是什么鬼

ThreadLocal是用于线程之间隔离的,但是InheritableThreadLocal可以使得子线程去自动拷贝来自父线程的副本数据。

简单的例子:

 public static void main(String[] args) { InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>(); threadLocal.set(1); System.out.println("父线程的副本值:" + threadLocal.get()); new Thread(() -> System.out.println("子线程的副本值:" + threadLocal.get())).start(); }

两个线程的副本值都是1,说明子线程确实自动拷贝了父线程的副本值。

原理很简单:

InheritableThreadLocal继承了ThreadLocal,

重写了childValue方法,直接返回了传入参数值。因为InheritableThreadLocal默认不对原值进行转换,如果我们需要对原值进行转换的话,可以重写该方法。

重写了getMap方法,返回当前线程的inheritableThreadLocals,也是ThreadLocalMap类型。createMap则是懒加载该inheritableThreadLocals。

 protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }

说白了,当前的副本不在threadLocals存了,而是存在了inheritableThreadLocals中。

接着看Thread的构造方法:

 public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); ...省略无关代码 //inheritThreadLocals为true,父线程的inheritableThreadLocals也不为空 if (inheritThreadLocals && parent.inheritableThreadLocals != null) //则利用父线程的inheritableThreadLocals去创建子线程的inheritableThreadLocals this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }

接着进入createInheritedMap方法:

 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; //将parentTable上key不为null的Entry复制到当前table上 for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //key.childValue返回e.value Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }

不过需要注意的是,如果在拷贝之后,父线程再进行set的话,子线程肯定是感知不到的。


六、ThreadLocal与内存泄漏

什么是内存泄露?

大白话讲,就是我自己创建的对象,在一系列操作后,我访问不到该对象了,我认为它已经被回收掉了,但该对象却一直存在与内存中。

那什么是内存溢出呢?

内存溢出是在有限的堆内存中(当然内存溢出的区域不止这一块)申请了大量的对象,造成oom的情况。

那两者的区别呢?

内存泄露比较严重的时候会导致内存溢出,如果每次gc后,堆内存都不能下降到一个比较低的占用量,那么可以使用jmap dump堆内存,再使用MAT找出导致内存泄漏的对象。

我们以一开头的例子来画出运行时的堆栈图:

thread 源码_Thread是什么 (https://mushiming.com/)  第3张

 为什么这里的key保持着对ThreadLocal的一个弱引用呢?保持强引用行不行?

假设这里的key保持对ThreadLocal的强引用,则当我的程序用不到该ThreadLocal时,我手动执行了tl=null,此时1号线断开,而这里的5号线是实线,5号线没有断开,因此ThreadLocal对象无法被回收掉,一直存在于内存中,造成内存泄露。

看来,这里的弱引用,能够保证用不到的ThreadLocal被回收掉。

弱引用就能完全防止内存泄露了吗?

由上面的分析,弱引用能够防止释放不掉ThreadLocal引起的内存泄露。但是,却不能防止释放不掉Integer引起的内存泄露。首先,执行tl=null,则1号线断开,GC到来时,5号线断开,此时ThreadLocal被回收掉了,这个key被置为了null,可是这个key对应的value强引用着Integer对象,该Integer无法在用户代码中访问到了,但却依然存在于内存中,造成内存泄露。

既然依然存在着内存泄露,那么JDK团队是怎么解决的呢?

从上文的源码分析来看,ThreadLocal中的get()、set()方法,不是单纯地去做获取、设置的操作。在它们的方法内部,依然会遍历该Entry数组,删除所有key为null的Entry,并将相关的value置为null,从而够解决因释放不掉value而引起的内存泄露。

有这些get()、set()方法,就能完全地防止内存泄漏吗?

但我们手动将tl置为null后,就已经没法调用这些get()、set()方法了。所以,预防内存泄露的最佳实践是,在使用完ThreadLocal后,先调用tl.remove(),再调用tl=null。tl.remove()能够使得ThreadLocalMap删除该ThreadLocal所在的Entry,以及将value置为null,tl=null使得ThreadLocal对象真正地被回收掉。
 

其实内存泄露的问题,核心在于ThreadLocal与Thread的生命周期不一致

有两种情况:

(1)ThreadLocal的生命周期长于Thread,此时的Thread销毁后,内部的ThreadLocalMap也逐渐销毁,这种情况是不会发生内存泄露的。

(2)在线程池相关的场景下,ThreadLocal的生命周期是明显短于Thread的。当ThreadLocal被置为null,而又没在其之前调用remove时,内存泄露就开始了,一直持续到Thread销毁。

还有哪些内存泄露的场景呢?怎么去解决呢?

【1】长生命周期的对象持有短生命周期对象的引用,就很有可能造成内存泄露。

长生命周期的对象往往和整个程序的生命周期相同,若是当它们持有短生命周期的对象的引用,尽管短对象不再被使用,也无法被垃圾回收器回收,因为垃圾回收器无法回收被强引用所关联的对象。

解决方案:

(1)像一些静态的集合类,它们的生命周期和整个程序相同,尽管放入集合中的元素不再需要,就算将元素强行置为null,但由于集合类持有它们的引用,这些元素占据的空间也得不到释放,那么在必要的时候,我们可以将集合类对象类型的变量置为null。

(2)单例模式中,单例与整个程序的生命周期一致,如果单例对象持有其他短对象的引用,也很容易造成内存泄露,这还得靠我们谨慎编码。

(3)又或是数据库连接对象(长生命周期),ResultSet与Statement对象(短生命周期),连接不再被使用时,需要调用其close()方法,释放长对象与短对象。同理,需要显式调用close()方法的长生命周期的对象还有Socket、IO流、Session等。

【2】非静态的外部类会隐式地持有外部类的一个强引用

在Android中,如果在Activity内声明一个非静态的内部类,那么只要该内部类没有被回收的话,那么外部类Activity就无法被回收,Activity所关联的视图和资源也不会被回收,这样的内存泄露比较严重。

解决方案:

(1)将非静态内部类改为静态内部类,静态内部类是属于类的,因此不会依赖于外部类的实例,从而不持有外部类实例的引用。

(2)显式地声明非静态内部类持有外部类的一个弱引用,被弱引用关联的对象,在下一次垃圾回收器活动时,就会被回收。


七、ThreadLocal的使用场景

ThreadLocal一个典型的场景就是,我们需要在某个线程内保存全局可流通的属性,避免参数传递的麻烦。

大可以使用拦截器,将请求的token信息解析成用户属性,放在ThreadLocal中,之后在该线程执行的任何地方都可以获取到用户属性。

不会真有人在不知道ThreadLocal的时候,一直把HttpServletRequest当作方法的常用参数吧?不会吧不会吧

当然,线程池以及异步程序中是不建议使用ThreadLocal的,你永远不知道你拿到的副本到底是哪个Thread的遗产。

此外,java8中的并行流也不是不建议使用ThreadLocal的,有关对并行流的介绍,可以移步我的另外一篇文章谈谈并行流parallelStream

THE END

发表回复