ThreadLocal 线程变量副本


  • 强引用:常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候

  • 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收

  • 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收

  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

1、ThreadLocal:线程的变量副本,每个线程 隔离

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

多个线程操作这个变量时,实际是操作自己本地内存里的变量,从而起到线程隔离的作用,避免了线程安全问题。

 

2、ThreadLocal数据结构

ThreadLocal  线程变量副本

 

 

(1)

==Thread类中存在ThreadLocal.ThreadLocalMap类型的实例变量ThreadLocal

==== 每个线程都有自己的ThreadLocalMap,ThreadLocalMap都有自己的独立实现

========key:ThreadLocal(实际上不是ThreadLocal本身,而是ThreadLocal的一个弱引用);key是ThreadLocal<?> k,继承自WeakReference(弱引用类型)

========value:代码中放入的值

====线程的写读

========每个线程在往ThreadLocal中写入值时,是存入自己的ThreadLocalMap

========在读取值是,是以ThreadLocal为引用,在自己的ThreadLocalMap中查找对应的key:ThreadLocal,对应的value,从而达到线程隔离(写入读取本身的ThreadLocalMap)

(2)区别于HashMap:

==HashMap是由数组+链表实现的

==ThreadLocalMap是没有链表结构,内部维护了Entry数组,每个Entry代表一个完整的对象。

 

3、ThreadLocal.set()原理

ThreadLocal  线程变量副本

 

 

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

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

(1)获取Thread中的threadLocals

(2)判断对应的ThreadLocalMap是否存在

==存在,就往ThreadLocalMap中set数据,key为ThreadLocal,value为要set的值

====若经过hash计算,对应的Map的位置的Entry为null,则直接将Entry放入

====若经过hash计算,对应的Map的位置有与当前的ThreadLocal hash计算后的key值相同的Entry,则直接更新该Entry的数据

====若经过hash计算,对应的Map的位置有Entry了,则线性向后遍历,直到遍历到Entry为null的位置,直接将Entry放入

====若经过hash计算,对应的Map的位置不为空,线性向后遍历时,遇到key为null(key过期)的Entry(即已被垃圾回收了)时,会执行replaceStaleEntry()方法,该方法是替换过期数据的逻辑,以当前key=null的Entry在Map中的下标index,staleSlot=slotToExpunge=index,开始遍历,进行探测式数据清理工作,以当前下标开始,向前查找其它过期数据,有则更新slotToExpunge=当前过期数据的下标index_new,直到遇到Entry=null,(如果一直往前探测没有遇到Entry=null,会从头部扫描跳到最尾部继续向前探测)然后更新开始查找的下标index为0,slotToExpunge=0;然后,从当前节点staleSlot(即第一次遇到的过期数据位置)开始,向后查找key值相等的Entry,找到后更新Entry的值,并交换下标为staleSlot的元素(过期元素)的位置,若在查找到key相等的Entry前遇到Entry为null的位置,就停止寻找,就会直接创建Entry(要set进入的Entry)替换staleSlot过期数据,然后开始进行过期Entry的清理工作.

==若不存在,就创建新的ThreadLocalMap并进行set

 

4、ThreadLocalMap的hash算法

(1)ThreadLocalMap实现hash算法来解决散列表数组的hash冲突问题

int i = key.threadLocalHashCode & (len-1);

(2)threadLocalHashCode的计算

==ThreadLocal一个属性:HASH_INCREMENT=0x61c88647

==每创建一个ThreadLocal对象,ThreadLocal.nextHashCode增长0x61c88647

==0x61c88647:斐波那契数,ThreadLocalMap的hash增量,hash分布非常均匀

public class ThreadLocal<T> {
   private final int threadLocalHashCode = nextHashCode();

   private static AtomicInteger nextHashCode = new AtomicInteger();

   private static final int HASH_INCREMENT = 0x61c88647;

   private static int nextHashCode() {
       return nextHashCode.getAndAdd(HASH_INCREMENT);
  }

   static class ThreadLocalMap {
       ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
           table = new Entry[INITIAL_CAPACITY];
           int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

           table[i] = new Entry(firstKey, firstValue);
           size = 1;
           setThreshold(INITIAL_CAPACITY);
      }
  }
}

 

5、ThreadLocalMap的hash冲突

(1)在插入过程中,通过hash计算

==若没有冲突,则直接放入ThreadLocalMap中

==若发生hash冲突,则在冲突的位置后线性向后查找,一直找到Entry为null的位置即Map为空的位置,并将该Entry放入该位置

==若遇到Entry不为null且key相等的情况,直接更新该位置的数据

==若遇到Entry中key为null的情况

====[1]记录当前过期数据位置index staleSlot=slotToExpunge=index;

====[2]从当前位置向前查找,每查找到过期数据,便更新slotToExpunge

              slotToExpunge=index_new(每找到的过期数据下标)

====[3]不断向前直到找到Entry=null(可能从头跳到尾继续查找),就停止查找,设置开始时的下标index为0 slotToExpunge为0

              index=0 ;slotToExpunge=0

====[4]从第一次遇到的过期数据即下标为index(此时index=0),开始向后查找

          若遇到Entry的key值与要插入的key值相等,则更新Entry的数据,并与下标index的Entry进行位置交换,并开始index的Entry的清理

          若在遇到相等key值前遇到过期数据或Entry为空的情况下,将要插入的Entry创建,直接替换下标index的Entry

 

6、ThreadLocalMap过期key的清理

(1)探测式清理 expungeStaleEntry()

==过程:

[1]从开始位置向后探测清理过期数据,将过期数据Entry设置为null

[2]探测过程中遇到Entry不为null的数据reEntry,进行rehash(重哈希计算),重新在数组中定位

[3]若rehash定位,定位到的位置有数据,则将数据reEntry放到最靠近这个位置的后边的Entry=null的位置

[4]如果再有其它数据set到map中,会触发探测式清理工作

[5]一直探测,直到遇到Entry=null,才停止探测

(2)启发式清理 cleanSomeSlots() :清理散列数组中Entry,key=null的数据。

==过程;

[1]如果在插入时遇到过期数据,下标index,此时slotToExpunge=stableSlot=index;

[2]从当前index向前开始查找其它过期数据时,直到找到Entry=null,也没有找到过期数据时

[3]向后查找过期数据进行Entry交换或者Entry新建直接替代插入时,也没有找到Entry=null或过期数据时

[4]此时,slotToExpunge==stableSlot==index,slotToExpunge不是等于0,则会调用cleanSomeSlots(expungeStaleEntry(slotToExpung),len),进行启发式过期数据清理,即直接清理当前slotToExpung的过期数据,然后才插入Entry新建。

 

7、ThreadLocalMap扩容机制

(1)进行set()方法后,如果执行完启发式清理工作后,没有清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就会执行rehash()

rehash()的阈值是    size>=threshold

if (!cleanSomeSlots(i, sz) && sz >= threshold)
   rehash();

(2)rehash()

private void rehash() {
   expungeStaleEntries();

   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);
  }
}

==[1]首先会进行探测式清理工作,从数组表的起始位置开始往后清理

==[2]清理完成后,数组可能有一些key=null的过期数据被清理掉,此时判断

判断size>=threshold-threshold/4      (true|false 来决定是否扩容)

(3)resize()

private void resize() {
   Entry[] oldTab = table;
   int oldLen = oldTab.length;
   int newLen = oldLen * 2;
   Entry[] newTab = new Entry[newLen];
   int count = 0;

   for (int j = 0; j < oldLen; ++j) {
       Entry e = oldTab[j];
       if (e != null) {
           ThreadLocal<?> k = e.get();
           if (k == null) {
               e.value = null;
          } 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;
}

==[1]创建新的扩容后的数组,扩容后的数组大小为 oldLen*2

==[2]遍历老的散列表,重新计算hash位置,将数据放入新的tab数组中

==[3]重新计算新表的阈值

 

8、ThreadLocalMap.get()

(1)set()方法:包括set数据、清理数据、优化数据表(rehash和扩容)

(2)

==第一种情况:

    通过查找key,计算出散列表中slot位置,若该slot位置中的Entry.key=key,则直接返回数据

==第二种情况:

[1]通过查找key ,计算出散列表中slot位置,若该slot位置中已经有了数据且Entry.key!=key,则需要继续向后查找

[2]如果遇到过期数据key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,清理过期数据,而后边的未过期数据会rehash()前移

[3]继续往后查找,直到遇到key值相等的Entry,则返回数据

[4]若往后查找时,遇到Entry=null时,则停止查找,表示没有该key对应的数据

(3)GC后key是否为空?

     ThreadLocalMap的key,即ThreadLocal是弱引用,在ThreadLocal.get()时,发生GC后,key是否为null?

==一般情况下,GC后弱引用会被垃圾回收,key会等于null,会出现value没被回收,key被回收,key=null,导致value一直存在,会出现内存泄漏OOM

==但此时,是ThreadLocal.get()操作,则说明有强引用存在,此时key还有对应的ThreadLocal,ThreadLocald的强引用还存在,则不会被垃圾回收,key不为null

ThreadLocal  线程变量副本

 

 

9、ThreadLocal内存泄漏问题OOM

(1)ThreadLocalMap中key值 ThreadLocal是弱引用

(2)弱引用,GC后会被垃圾回收,

(3)如果ThreadLocal被GC垃圾回收,此时ThreadLocalMap的key=null

(4)ThreadLocalMap的生命周期跟Thread一样的,此时,key==null,对应的value还存在,可能会造成内存泄漏问题OOM

(5)解决方法:使用完ThreadLocal后,及时调用remove()方法释放内存空。

原创文章,作者:jamestackk,如若转载,请注明出处:https://blog.ytso.com/245610.html

(0)
上一篇 2022年4月18日
下一篇 2022年4月18日

相关推荐

发表回复

登录后才能评论