ThreadLocal 线程变量副本
作者:互联网
-
强引用:常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
-
软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
-
弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
-
虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
1、ThreadLocal:线程的变量副本,每个线程 隔离
ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
多个线程操作这个变量时,实际是操作自己本地内存里的变量,从而起到线程隔离的作用,避免了线程安全问题。
2、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()原理
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
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()方法释放内存空。
标签:过期数据,副本,ThreadLocalMap,ThreadLocal,线程,key,Entry,null 来源: https://www.cnblogs.com/hexiayuliang666/p/16158867.html