Java多线程与高并发七(ThreadLocal源码)

编程入门 行业动态 更新时间:2024-10-28 17:20:35

Java<a href=https://www.elefans.com/category/jswz/34/1767532.html style=多线程与高并发七(ThreadLocal源码)"/>

Java多线程与高并发七(ThreadLocal源码)

ThreadLocal是什么

ThreadLocal提供了线程局部变量,由该类保存的变量,会分开线程,不同的线程会保存不同的变量副本。

import java.util.HashMap;
import java.util.Map;public class ThreadLocalTest {ThreadLocal threadLocal = new ThreadLocal();Map<String, String> map = new HashMap<>(16);public static final String KEY = "key";String s1 = "dog";String s2 = "cat";public void f1() {threadLocal.set(s1);System.out.println("f1通过threadLocal获得的字符串是:" + threadLocal.get());map.put(KEY, s1);System.out.println("f1通过map获得的字符串是:" + threadLocal.get());}public void f2() {System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());System.out.println("f2通过map获得的字符串是:" + map.get(KEY));threadLocal.set(s2);map.put(KEY, s2);System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());System.out.println("f2通过map获得的字符串是:" + map.get(KEY));}public static void main(String[] args) throws Exception {ThreadLocalTest tlt = new ThreadLocalTest();new Thread(tlt::f1).start();Thread.sleep(2000);System.out.println("-----------------------------------------");new Thread(tlt::f2).start();}
}

示例代码很简单,测试了ThreadLocal与map的区别,可以看到结果显示如下:

f1通过threadLocal获得的字符串是:dog
f1通过map获得的字符串是:dog
-----------------------------------------
f2通过threadLocal获得的字符串是:null
f2通过map获得的字符串是:dog
f2通过threadLocal获得的字符串是:cat
f2通过map获得的字符串是:catProcess finished with exit code 0

结果意味着,ThreadLocal存储的变量是线程独占的,f1方法开始的线程设置的threadLocal变量s1,在f2方法开始的线程中并拿不到。

那我们知道了ThreadLocal保存的变量实际上是线程隔离的。

ThreadLocal之get()方法

我们先从简单的get方法看起,看看ThreadLocal是如何实现线程隔离来获取设置的变量的。

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T)e.value;return result;}}return setInitialValue();}

可以看到原码中获取了当前线程t,又从当前线程中获取到了一个map,有点看不懂啊,这个ThreadLocalMap什么鬼?

我们看看ThreadLocal结构:

可以看到,ThreadLocalMap是ThreadLocal的内部类,而getMap方法呢?

    ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

很简单,就是从当前线程中拿到一个对象threadLocals,这个对象来自ThreadLocal的内部类ThreadLocalMap

如果map不为空,又去拿map中的entry,如果entry不为空,就把entry的值返回,整个获取过程就在最理想的情况下完成了。

Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用(作用后面再谈)。

我们看看map.getEntry方法干了些什么事?

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}

这里会传入ThreadLocal对象,然后把threadLocal的hash码与(table.length-1)做与运算,拿到Entry数组的下标,这里值得一提的是为啥table的注释会那样写:

        /*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;

说这个table的长度必须是2的次方,因为要服务于取下标的【与运算】(&),其运算规则是:

         

运算规则:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1;

如果table的长度是2的次方,那么table.length-1的二进制码就会是:1111···,比如:

table.length-1二进制码
-1    (其值为7)111
-1    (其值为15)1111
-1    (其值为31)11111

做与运算的时候,结果才会均匀分布。因为如果二进制码是0,不论与什么(0或1)运算都是0,其结果唯一,不具有均匀分布特性。

拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(e.get()方法的返回值就是),额,这里需要看看Refrence类的源码:

public abstract class Reference<T> {//从英文注释看出来,该referent根据引用类型不同会被GC区别对待private T referent;         /* Treated specially by GC */public T get() {return this.referent;}/* -- Constructors -- */Reference(T referent) {this(referent, null);}Reference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue;}//省略其他代码
}

其实get()方法无非就是返回当前类的一个泛型对象。而这个类跟我们说的源码的关系就在于Entry继承了WeakRefrence,而WeakRefrence又是Refrence的子类。

Refrence类设计出来的目的就是让咱们自定义的类A继承它,相当于给咱们的类A穿上一层外套,方便垃圾收集器识别何时应该对类A的对象进行回收。

-----------------------------------------------------------------------补充知识开始-----------------------------------------------------------------------

1、对象的引用类型有强软弱虚四种

强引用:A  a = new A();  普通new对象,就是强引用

软引用:new SoftReference(new A()),当内存不够用时,优先收集软引用所占用的内存

弱引用:new WeakReference(new A()),每当发生GC,都会收集该引用指向对象所占用的内存(ThreadLocal的Entry用到了)

虚引用:new PhantomReference(new Object (),QUEUE),垃圾回收也是见一次回收一次,但是回收后,有一个通知到其队列里,用来控制堆外内存回收用。就是当这个引用指向的对象被回收时,虚引用的队列里有一个通知,应该是指向系统内存的引用,再用c语言之类的底层语言回收堆外内存

2、reference指引用本身,referent指的是被包裹的对象(谁继承上图的类,谁就被包裹)

-----------------------------------------------------------------------补充知识结束-----------------------------------------------------------------------

我们前面说到:Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用,现在说说继承弱引用的作用:

方便每次GC都把Entry对象都给回收掉。

ThreadLocal的结构图表明,Entry是ThreadLocal内部的静态内部类ThreadLocalMap内部的静态内部类,我们看看源码:

    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;}}//省略代码无数}

源码表明Entry其构造方法调用了super(k),也就是Refrence的构造方法,完成对refrent的赋值。也就是前面e.get()方法的返回值。

那好,既然Entry的构造传入的ThreadLocal的对象k,那么e.get()方法取出来,也应该是ThreadLocal类的一个对象。

回到getEntry的源码:

 private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}

拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(ThreadLocal的一个对象k)也是与ThreadLocal的对象key相等,那么就返回table的第i个元素。

如果e.get()取出的引用与传入的ThreadLocal对象key不相等,那么说明,有可能发生了垃圾回收,弱引用遇到垃圾回收,是像老鼠过街,过一次,被收拾一次。如果发生了垃圾回收,ThreadLocal的get()方法就会调用getEntryAfterMiss方法。

我们看看getEntryAfterMiss方法的源码:

       /*** Version of getEntry method for use when key is not found in* its direct hash slot.** @param  key the thread local object* @param  i the table index for key's hash code* @param  e the entry at table[i]* @return the entry associated with key, or null if no such*/private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}

可以看到,getEntryAfterMiss方法传入的参数有计算好的下标,以及可能为空的e = table[i](或者e.get()取出的值不是当前对象,e.get()此时多半是null)。

1、如果e = table[i]为空,就直接返回为null,表明真的没有该Entry,即总的来说,ThreadLocal中没有该线程存储的私有值。

2、如果e = table[i]不为空,尝试从e.get()方法中取,如果这次判断与k==key(上面有分析),就返回该entry;如果e.get()取出的ThreadLocal变量不为空,又不与传入的key相等,数据过时了(垃圾回收删除了引用),应该获取下一个下标,直到找到当前线程存入的那个key为止。

而nextIndex的实现就简单了:

        /*** Increment i modulo len.*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

就是判断当前获取的下标i的下一个是不是小于table的长度,如果小于table的长度,就获取下一个下标,超过table的长度就用0。这里可以看出,Entry数组逻辑上是环形结构。

3、最后,取出table[i](代码中的e=tab[i],这里的i已经自增,变了哟),赋值给e,当下一次循环的时候,再次经过上述流程的判断,直到找到相同的key,把table[i]返回。

4、最麻烦的一点,如果e.get()方法获取的弱引用为空,但是此时的e=table[i]不为空,表示有gc活动清除了弱引用,这时应该把所有的老旧的引用都清除,重新为该entry建立新的弱引用。

虽然,ThreadLocal获取线程的私有变量副本看起来不需要参数,但是其内部实现是取出了当前线程对象,利用了当前线程对象的属性threadLocals(ThreadLocalMap类),获取threadLocals的内部类entry对象时,设定了参数this(当前ThreadLocal的对象引用),那实际上是内部的键值存储就是:

ThreadLocal内部存储键值
ThreadLocal当前对象引用(this)存入的Obj

 

现在梳理下threadLocal.get()的流程:

 ThreadLocal之set(T value)

往ThreadLocal里加入线程隔离的私有数据,会用到ThreadLocal的set方法,因为线程隔离,所有set也没有显式的键。

    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

前面两行代码都与获取一致,获取到当前线程中的定义在ThreadLocal中的ThreadLocalMap,为空则创建,不为空,就设值。

我们先来看看简单的createMap

 void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

就是new了一个ThreadLocalMap赋值给当前线程的threadLocals变量。注意,这个指的是ThreadLocal当前对象。结构上面我们上面展示的表格。

再看看map.set(this,value)干了啥:

      private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//利用key计算table的下标i,均匀分布原理如上述【与运算】分析int i = key.threadLocalHashCode & (len-1);//hash冲突走for循环for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//取出该entry的弱引用ThreadLocal<?> k = e.get();//k未被GC删除,并且与key能匹配上,覆盖存值,返回if (k == key) {e.value = value;return;}//k是空的情况,弱引用被清除if (k == null) {replaceStaleEntry(key, value, i);return;}}//hash不冲突走这边tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}

set分析一:hash不冲突的情况

当hash不冲突的时候,把value放到了一个new出来的Entry里面,在赋值给Entry数组table。

接下来,set方法做了一件事,即:判断是否能清理掉一些table中已经被GC回收的弱引用,如果发现了有Entry存在,但是其弱引用被清除的,那么cleanSomeSlots返回true,不会执行rehash方法。

private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);//n决定了扫描的次数return removed;}

while循环即使n传入的值是16,也只循环四次,所有大多数情况,该cleanSomeSlots方法只会循环3到4次(n>>>1 与 n=n/2取整,差不多)。

假如i算出来是8,n是4,那么在逻辑上,n的变化是4-->2-->1-->0,会循环三次,会查找table的9、10、11的entry是否存在以及是否其弱引用被删除。

这时候有两种情况:

1、8号后面仨,肚子装了entry,弱引用也被清除了,这时候set是不会触发rehash的

2、8号后面三,可能entry为空,可能不为空但是弱引用还在(e.get()!=null),这时候cleanSomeSlots返回false,再判断出当前table中如果size大于2/3的table容量时,会触发rehash

在cleanSomeSlots方法中的while循环中,如果ThreadLocal在set时计算的当前下标i对应的下一个下标的table[i]有entry元素,并且其弱引用已被清除,也就是:

 if (e != null && e.get() == null)

这个条件满足,那么就会执行expungeStaleEntry方法,清理掉过时的Entry,这里是直译的stale,我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry。

这里必须看看expungeStaleEntry方法的源码了:

 private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清除数组table中给定下标的元素tab[staleSlot].value = null;tab[staleSlot] = null;size--;// 循环table中的元素,从给定的staleSlot的下一个元素开始,遇到null就rehashEntry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//糟糕,下一个元素的弱引用也被清除了if (k == null) {// 那就也清除数组table中给定下标的元素e.value = null;tab[i] = null;size--;} else {//下一个元素的弱引用未被清除的情况//重新给该元素计算下标int h = k.threadLocalHashCode & (len - 1);//计算的下标不重样的话if (h != i) {//原下标的元素置为空tab[i] = null;//反复查询新下标的值是否有元素占用while (tab[h] != null) {//占用了就取下一个h = nextIndex(h, len);}//没占用就把e赋值到table新下标的位置中tab[h] = e;}}}return i;}

源码中可以知道,expungeStaleEntry的方法不仅删除给定下标的元素,连带着该下标的后续下标也会受到检查,如果也是stale entry,那么会被清除掉,如果不是stale entry就会被重新计算下标再赋值到table中(这步操作不明其意图,如果有看官大佬研究透了,欢迎留言调教),最后会返回【不是连带清除么,清除到哪个位置了?】的下标。

set分析二:hash冲突的情况

源码这么多,您记得住?我可记不住,再看看说到set方法哪里了:

冲突的时候也分两种情况:

1、table中的i下标位置虽然有entry,但是i的nextIndex的弱引用与当前key一致,就进行覆盖操作

2、i的nextIndex的弱引用被清除,那么就执行replaceStaleEntry方法

stale entry即上述的【我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry】,从方法名来看,是替换掉stale entry,我们来一探究竟:

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// 往table数组的给定下标的前面找,只要该位置有值,弱引用被清除,就记录下该位置// 重复找,slotToExpunge的位置只记录往前找到的最后一个int slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 往table数组的给定下标的后面找,只要不为空for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果k和key相等,就替换掉k中的entry的value为新传入的value// 并且把table中的staleSlot和staleSlot的下一个交换if (k == key) {//e是过时槽的下一个槽,先附上值valuee.value = value;//下一个槽保存了过时槽的数据,等待被删除tab[i] = tab[staleSlot];//过时槽装入过时槽下一个槽的数据,交换完成,可以清除过时槽的entry了tab[staleSlot] = e;// Start expunge at preceding stale entry if it existsif (slotToExpunge == staleSlot)slotToExpunge = i;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.if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
replaceStaleEntry方法弊端是:会清除给定下标i附近所有的连续的stale entry,如下图:假如staleSlot(也就是某个下标)是4,那么2、3、4、5、6都会被清除掉

至此,ThreadLocal的源码算是大部分看完了。打个哈欠,睡觉。

更多推荐

Java多线程与高并发七(ThreadLocal源码)

本文发布于:2024-03-13 08:57:12,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1733618.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:多线程   源码   Java   ThreadLocal

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!