ThreadLocal 源码分析
ThreadLocal 源码分析
1、ThreadLocal 源码分析
在多线程开发中,我们经常会使用ThreadLocal来避免共享变量的竞争,提高效率。ThreadLocal底层到底是怎么实现的呢,今天就带大家一起来看看它底层实现。另外也会随便分析下网上讨论比较多的关于ThreadLocal内存泄漏等等究竟是怎么一回事
我本地的jdk版本是11.0.8,不同版本的jdk,threadLocal源码实现可能有差别,不过大致是一样的。
首先看下我们一般都是怎么使用ThreadLocal的
这是一段使用threadLocal的demo代码
public class ThreadLocalDemo { public final static ThreadLocal<String> threadLocal = new ThreadLocal<>() { @Override protected String initialValue() { return "initValue"; } }; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("init Value =" + threadLocal.get()); threadLocal.set("abc"); System.out.println("执行其他逻辑"); String str = threadLocal.get(); System.out.println(str); threadLocal.remove(); }).start(); Thread.currentThread().join(); } }
这段代码比较简单,不用具体说threadLocal的这些方法都是干啥的了。我们直接顺着main方法里面的调用顺序一起去看看这些调用背后都是怎么实现的。
如果直接把类的源码粘上来,做分析,感觉太零散了,看不到方法之间的调用关系,所以,这次就准备按照调用逻辑,一步一步来分析了
首先我们main方法是在第11行调用了threadLocal.get(),这是我们第一次主动调用threadLocal的地方,那我们先从这里进去
//这就是ThreadLocal的get方法了,我们泛型参数是String,所以这里的T也就是String了,下面我们一行一行根据我们上面的Demo来分析下这个代码 public T get() { //这一行就不用说了,Thread.currentThread()就是获取当前线程 Thread t = Thread.currentThread(); //getMap(t)这行是干什么呢?我们这个方法的代码比较简单,我就直接粘贴到下面了 //ThreadLocalMap getMap(Thread t) { // return t.threadLocals; //} //上面这3行就是getMap(t)调用的代码了,比较简单,获取线程t上的threadLocals属性,这个属性是什么东西呢?我们再看看这个属性 //ThreadLocal.ThreadLocalMap threadLocals = null; //上面这一行就是在Thread类中定义的threadLocals属性了,看样子是ThreadLocal类中的内部类ThreadLocalMap的一个实例,具体是啥,我们先不仔细看了,继续看后面代码吧 ThreadLocalMap map = getMap(t); //这里返回的map当前是null,因为Thread的threadLocals,从来没有初始化过 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //因为map==null,所以会调用到这个setInitialValue这个方法,从这里返回,我们继续看看这个方法吧 return setInitialValue(); }
private T setInitialValue() { //这里的initialValue方法是protected的,默认返回null,由于我们上面Demo第4行重写了initialValue方法,所以这里的调用就是我们上面的代码,这里的返回应该是上面我们Demo第5行的"initValue" T value = initialValue(); //这几行代码和上面get方法是一致的 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //map==null if (map != null) { map.set(this, value); } else { //会走到这里来,我们去这个createMap方法看看 createMap(t, value); } //这里的this是通过匿名继承的ThreadLocal的,不会走到这个instanceof内部去 if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } //我们上面的Demo第4行的threadLocal.get()调用最终就会从这里返回,返回值是"initValue" return value; }
//这里就是初始化Thread的threadLocals属性了,后面这个属性就不是null了,这里创建了个ThreadLocalMap对象,有两个参数,第一个this就是我们Demo中调用threadLocal.get()方法的对象,也就是Demo第4行的threadLocal对象,firstValue就是上面的字符串"initValue"。//我们进到ThreadLocalMap构造方法去看看 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
// ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //这里首先会创建一个Entry的数组,INITIAL_CAPACITY = 16;也就是这里创建一个大小是16的Entry数组,Entry是啥,我们下面再看,先看看这个构造方法的其他几行代码 table = new Entry[INITIAL_CAPACITY]; //threadLocalHashCode是ThreadLocal的成员变量, // private final int threadLocalHashCode = nextHashCode(); //上面是它的定义,从上面可以看到每次创建ThreadLocal对象时就会初始化threadLocalHashCode,它的值是通过静态方法nextHashCode()赋值的,每调用一次nextHashCode返回值就在上次的基础上增加0x61c88647。 //这里的i就是threadLocalHashCode的值和(INITIAL_CAPACITY - 1)进行与运算,这里的(INITIAL_CAPACITY - 1)值是15,计算结果在0-15之间,作为数组的下标 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //这里创建个Entry对象,赋值给table中下标为i的 table[i] = new Entry(firstKey, firstValue); //这里的size是table数组中元素的个数 size = 1; //这里是设置threshold = len * 2 / 3;当size的值超过threshold时,table数组就会扩容成原来数组的2倍 setThreshold(INITIAL_CAPACITY); }
//这里我们看看Entry对象//这就是Entry的源码了,更简单。继承了WeakReference对象,这里会把构造方法的ThreadLocal入参包装成弱引用。具体啥是弱引用,下面粘贴上一段《深入理解java虚拟机》上面的描述。放到我们这里来说,就是ThreadLocal变量在没有其他地方引用(只有在Entry这里有引用),当下次垃圾回收的时候,就会被回收掉 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; // Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
下面是《深入理解java虚拟机》上面的关于引用的描述
到这里上面Demo第4行的源码就全部看完了,下面简单总结下执行逻辑。
首先进入ThreadLocal的get方法,获取当前线程的threadLocals变量,我们这个变量没有初始化过,所以这个变量为空,继续执行setInitialValue()方法,并从这里返回
在setInitialValue方法中首先调用我们Demo中重写ThreadLocal的initialValue方法,获取返回值。
继续调用createMap方法,
创建ThreadLocalMap对象,内部创建Entry数组,将threadLocal对象包装成弱引用及initialValue方法的返回值创建Entry对象,填充到Entry数组数组中
将创建的ThreadLocalMap对象赋值给当前线程的threadLocals变量
将2中获取的值返回
现在我们看下Demo中的第12行threadLocal.set("abc")
//这就是ThreadLocal的set方法了,和get方法差不多 public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的threadLocals的属性,在上面get方法已经设置过这个属性了 ,所以这里不为空了 ThreadLocalMap map = getMap(t); if (map != null) { //map不为null,就会走到这里了,这里的this就是我们Demo中继承ThreadLocal的匿名内部类,value就是"abc",下面我们重点去看下这个方法 map.set(this, value); } else { createMap(t, value); } }
//这个方法是ThreadLocal的内部类ThreadLocalMap的。 private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. //这个是ThreadLocalMap构造方法创建的table,是Entry数组 Entry[] tab = table; int len = tab.length; //这个是获取根据key计算在数组中的下标,考虑到有可能两个key计算出来的是同一个i,所以数组中下标i不一定就是我们需要的key,会从当前i的下标向后遍历。同样根据key获取值的时候,也会有类似情况 int i = key.threadLocalHashCode & (len-1); //获取下标i的值Entry,进行遍历,直到entry.key==我们的入参key或者entry==null或者entry.key==null(这个场景就是key其他地方没有引用了,只有Entry有对应的弱引用,在下次垃圾回收后,entry.key就会==null)的情况下,结束循环 //注意:这里不会出现数组中所有下标都满了,且entry.k!=key的场景,因为下面的后面会判断size>=threshold,进行扩容 for (Entry e = tab[i]; e != null; //考虑到可能会有冲突,也就是上面说的两个key计算出来的是同一个i,所以key有可能不存在对应下标i的位置 //nextIndex(i, len)向后获取下一个位置是循环的,如果达到i==len-1;这时i=0;就会从数组头元素开始,后面几个方法说的向前遍历,向后遍历都是类似的 e = tab[i = nextIndex(i, len)]) { //获取Entry中的key,这里是个弱引用,通过get方法获取弱引用的实际对象。 ThreadLocal<?> k = e.get(); //找到了对应的key,就设置新值,返回 if (k == key) { e.value = value; return; } //k==null,这个场景说了,就是外部没有对ThreadLocal对象的其他引用了(强引用),GC释放后,k就是null,jiu就会走到这里 if (k == null) { //这个方法会从当前下标i开始,删除过期的entry,重新设置新的Entry到i的位置 replaceStaleEntry(key, value, i); return; } } //走到这里,这时下标i对应的Entry已经是null了 tab[i] = new Entry(key, value); //数组中元素个数+1 int sz = ++size; //这里会移除一些过期的entry,判断sz>= threshold,如果成立,就扩容数组 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
//这个方法主要是 private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { //这里的for循环都不会陷入死循环,因为上面有 sz >= threshold //获取Entry数组 Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; //从当前入参staleSlot开始向前遍历,直到下标i对应的Entry==null for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) //随着i向前遍历,slotToExpunge的值会逐步更新,直到staleSlot和它之前第一个tab[i]==null之间,e.get()==null的下标,如果threadLocal对象外部一直有引用,那e.get()==null,就不会为null,也不会走到这个if分支 if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first //从当前入参staleSlot开始向后遍历,直到下标i对应的Entry==null for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { //获取到对应的Entry的threaLocal变量 ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. //如果k==key,这时就是更新value就可以了 if (k == key) { e.value = value; //这里会进行元素交换,把找到的Entry换到入参staleSlot的位置 //注意当前staleSlot元素位置是过期的,需要清理的,交换后i下标位置的元素就是需要清理的 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //这个条件成立的话,需要第一个for循环中if分支没有进入,也就是不存在staleSlot和它之前第一个tab[i]==null之间,e.get()==null if (slotToExpunge == staleSlot) //走到这里说明staleSlot之前到tab[i]==null之间没有无效元素,我们上面进行了元素交换,这时i就是第一个过期的元素了 slotToExpunge = i; //清理无效的元素,slotToExpunge就是staleSlot之前到tab[i]==null到之后到tab[i]==null这段元素之间第一个过期元素的下标位置 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. //k==null说明当前数组已经有entry失效了,slotToExpunge == staleSlot说明staleSlot之前没有失效的,这时就要清理后面的过期元素 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot //入参的staleSlot下标已经个个过期的值,将value设置为null,重新赋值个新的entry tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) //走到这里说明staleSlot向前遍历或者向后遍历中出现了k==null,这时需要清理过期的entry cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
//从staleSlot位置开始,直到数组中entry==null。清理在这过程中数组过期的元素,并调整那些元素有效,但是下标位置不正确的元素位置 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot //清空对应staleSlot对应的元素,size-1 tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; //在staleSlot开始向后遍历,直到数组中entry==null for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //如果k==null,就清空元素,size-1 if (k == null) { e.value = null; tab[i] = null; size--; } else { //h是算法计算出来元素应该在数组中的下标位置 int h = k.threadLocalHashCode & (len - 1); //i是元素的直接存储下标,由于碰撞的原因元组有可能不是存在算法计算出来的下标位置 if (h != i) { //h!=i说明元素根据算法出来的下标和实际存储下标不一致,这时由于我们清空了一些过期元素,这时就需要重新调整这些有效的,算法计算出来的下标,和实际存储下标不一致的元素位置 tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. //从h开始向后遍历,找到数组中的null位置,将当前下标i位置上的元素设置到对应位置 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } //这里的i就是for循环中退出条件,staleSlot开始向后遍历,数组中第一个entry==null的位置 return i; }
//从i开始清理无效的元素 //注意下标i的位置不是无效元素,要不下标i位置==null,要不是有效元素 private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //获取i后面的位置 i = nextIndex(i, len); Entry e = tab[i]; //如果i位置的元素是过期的,就执行清理 if (e != null && e.get() == null) { n = len; removed = true; //这里上面说的清理过期元素,调整位置不正确的有效元素的位置 i = expungeStaleEntry(i); } //这里的n>>>=1主要是扫描次数,应该是出于效率的考虑 } while ( (n >>>= 1) != 0); return removed; }
//这就是扩容了 private void rehash() { //这里首先清理数组中所有的过期元素,同时会调整不正确元素的下标 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis //由于我们上面会清理过期元素,所以size有可能变小,有可能就不需要扩容了,所以这里重新判断是否需要扩容,如果需要就进行扩容 if (size >= threshold - threshold / 4) resize(); }
//这里就是扩容了,数组长度变成原来的2倍,重新设置threshold和size private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //长度设置成原来的2被 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (Entry e : oldTab) { 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; }
上面就是Demo第12行 threadLocal.set("abc")方法涉及的所有代码了 。
下面总结下:
1.获取当前线程的threadLocals变量,如果不存在就调用createMap(t, value),创建并设置值,我们Demo第11行threadLocal.get()就走的这里
2.如果当前线程的threadLocals变量存在,就调用map.set(this, value)设置值
3.在获取值的过程中首先根据调用者threadLocal对象计算出应该存储在数组中的下标,
如果当前下标对应的数组元素是null,就新生成Entry元素放入数组下标i的位置,数组size+1,判断是否需要扩容,如果需要就对数组进行扩容
如果当前下标对应的数组元素不是null,就获取对应位置的元素进行判断,如果entry.key == 我们调用的threalocal对象,就更新value返回。如果entry.key==null,说明元素释放了就调用replaceStaleEntry进行处理。如果这两种情况都不是,那就继续从当前下标开始向后遍历,继续判断处理
最后看下Demo中第16行threadLocal.remove();
//获取当前线程的threadLocals变量,如果之前调用过get,set方法,那这时这个变量就不是null了 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { //走到这里看看移除元素的方法,这里的this,是我们方法的调用者,也就是threadLocal实例对象 m.remove(this); } }
//这个方法就比较简单了,计算threadLocal下标,从这个位置开始向后遍历,对应元素(这里使用的是key==,所以找到的肯定是同一个对象),将Entry中引用的threadLocal对象清空,再执行清理过期元素的动作 private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
上面remove方法也讲完了,这个比较简单。就是找到t.threadLocals中对应的元素Entry,调用e.clear()清理掉entry引用的threadLocal变量(这时,通过e.get()获取的threadLocal元素就是null了),然后调用expungeStaleEntry执行过期元素的清理。
2、日常使用注意
上面就是我们日常使用threadLocal中方法的源码了 ,通过上面代码对于调用的内部细节我们也基本看到了,下面说一些日常使用过程中需要注意的地方。
它们的结构简单画个图,就是下面的样子了
文章开始的Demo只是一些日常使用,通过上面的源码阅读,我们其实还是可以看到一些其他的使用方式和注意地方
我们一般是在多个线程中是使用同一个threadLocal对象,其实我们也可以在一个线程中使用多个threadLocal对象(同样多个线程也就可以使用多个threadLocal对象),就像下面这样
//创建多个ThreadLocal对象 ThreadLocal<String> threadLocal0 = new ThreadLocal<>(); ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); new Thread(() -> { //在一个线程中使用多个threadLocal对象 //通过上面源码我们看到threadLocal对象只是用来寻找当前线程中threadLocals变量中数组的位置,并读取或者设置值。由于查找元素下标用的是==,所以无论怎么找到的都是同一个对象实例 threadLocal0.set("th0"); threadLocal1.set("th1"); System.out.println(threadLocal1.get()); System.out.println(threadLocal0.get()); }).start(); Thread.currentThread().join();
内存释放相关
上面的源码分析部分,我们看到了存储到threadLocals中Entry的threadLocal变量是弱引用了,而弱引用在下次gc的时候就会被清理,threadLocal被清理了,上面的清理过期元素的方法就会把对应Entry进行清理。但是这里有个前提,threadLocal只有弱引用的时候才会被清理,如果有强引用存在,就不会被清理。
看下面的例子
ThreadLocal<String> threadLocal0 = new ThreadLocal<>(); new Thread(() -> { //我们第一行定义的地方,threadLocal对象是个强引用,只要这个强引用存在,threadLocals中对应的Entry就不会被清理 threadLocal0.set("th0"); //这个也很好理解,如果被清理掉了,那我们这里的get方法就获取不到值了,我们见过之前set后,对应get获取不到值的情况吗,肯定没有么 System.out.println(threadLocal0.get()); //所以我们想要清理存放的元素Entry就需要调用threadLocal0.remove()进行清理,或者是调用 threadLocal0=null;方法,这样当前 threadLocal0就只有线程threadLocals中对应的弱引用,这时,gc才会清理掉entry.k,对应元素才会在清理元素方法中清理掉 }).start(); Thread.currentThread().join();
另外由于ThreadLocal的操作都是基于当前线程的threadLocals变量的,如果当前线程不存在了,被清理掉了 ,只要threadLocals变量和内部Entry的key和value,如果没有其他地方进行引用,也都会被gc清理掉。
以上就是关于ThreadLocal的全部内容了。
另外父线程和子线程之间传递参数可以通过inheritableThreadLocals,这个变量的设置实在Thread的构造方法中,和threadLocals一样,都是ThreadLocal.ThreadLocalMap的实例。这个用的不多,这里就不说了。
来源https://www.cnblogs.com/wbo112/p/14969044.html