文章内容引用自 咕泡科技 咕泡出品,必属精品
文章目录
- 1ThreadLocal使用
- 2ThreadLocal原理源码分析
- 2.1get方法
- 2.1.1入口
- 2.1.1.1ThreadLocal中的ThreadLocalMap对象
- 2.1.1.2强引用
- 2.1.1.3软引用
- 2.1.1.4弱引用
- 2.1.1.5虚引用
- 2.1.2 初始化方法
- 初始化创建Map
- 2.2Set方法
- 2.2.1.set入口
- set方法
- 2.3remove方法
- 2.4 扩容逻辑
- 2.5其他非正常情况
- 2.5.1当ThreadLocal与ThreadLocal1的hash值冲突
- 2.5.2当Key被GC回收处理
- 3为什么Key要弱引用?
- 4Value为什么不用弱引用
- 怎么解决hash冲突
1ThreadLocal使用
我们知道,一个共享变量或者资源,在多个线程操作的时候,肯定是会相互影响,不能隔离的
public class AtomicTest {
int i=0;
public void incr(){
i+=10;
System.out.println(i);
}
public static void main(String[] args) throws InterruptedException {
AtomicTest test=new AtomicTest();
Thread[] threads=new Thread[5];
for (int j = 0; j < 5; j++) {
threads[j] =new Thread(() -> {
test.incr();
});
threads[j].start();
}
for (int j = 0; j < 5; j++) {
threads[j].join();
}
}
}
比如,这样的代码,那么得到的结果,肯定是每次加10不定,还有因为原子性问题,肯定会出现相同的10.20…。
那么假如我有场景,我要做到线程之前的数据相互不影响!!相互隔离,也就是我们讲的线程安全。比如mybatis里面,sqlsession就是存在ThreadLocal里面的,sqlSession这个对象就是线程安全的!!那么实现方式就是我们今天讲的重点:threadLocal
上面的栗子怎么 变得香甜 实现线程安全?如下:
public class ThreadLocalTest {
ThreadLocal<Integer> integerThreadLocal=new ThreadLocal<Integer>() {
public Integer initialValue() {
return 10;
}
};
ThreadLocal<Integer> integerThreadLocal1=new ThreadLocal<Integer>() {
public Integer initialValue() {
return 20;
}
};
public void incr() {
int value = integerThreadLocal.get().intValue();
integerThreadLocal.set(value += 10);
int value1= integerThreadLocal1.get().intValue();
integerThreadLocal1.set(value1 += 10);
System.out.println(integerThreadLocal.get());
System.out.println(integerThreadLocal1.get());
}
public static void main(String[] args) {
ThreadLocalTest test=new ThreadLocalTest();
Thread[] threads=new Thread[5];
for (int j = 0; j < 2; j++) {
threads[j] =new Thread(() -> {
test.incr();
});
}
这样我们integerThreadLocal得到的结果都是20,integerThreadLocal1得到的结果都是30。
2ThreadLocal原理源码分析
我们从2个角度去分析源码,一个是get 一个是set
2.1get方法
2.1.1入口
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//去根据Thread拿ThreadLocalMap,我们发现Thread类下会有个数据对象叫做ThreadLocalMap
//1.1 线程第一次进来,map肯定是null
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//1.2进入初始化方法
return setInitialValue();
}
2.1.1.1ThreadLocal中的ThreadLocalMap对象
ThreadLocalMap对象,里面有个Entry的 key 、value的结构
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的key是一个对于ThreadLocal这个对象的弱引用。
讲下四大引用 除了基础的数据类型外都是引用类型,那么java根据其生命周期的长短又将引用类型分为强引用、软引用、弱引用、虚引用。
2.1.1.2强引用
也是我们平时用得最多的,new一个对象就是强引用,例如 Object obj = new Object(); 当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 如果将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了
当内存不足的时候,jvm开始垃圾回收,对于强引用的对象,就算出现OOM也不会回收该对象的。 因此,强引用是造成java内存泄露的主要原因之一。
public static void main(String[] args) {
Object obj=new Object();//这样定义就是一个强引用
Object obj2=obj;//也是一个强引用
obj=null;
System.gc();
//会不会被垃圾回收?
System.out.println(obj2);
}
2.1.1.3软引用
软引用的生命周期比强引用短一些。软引用是通过SoftReference
类实现的。当JVM认为内存空间不足时,就会去试图回收软引用指向的对象对于只有软引用的对象来说, 当系统内存充足时,不会被回收; 当系统内存不足时,会被回收;
Object obj=new Object();
SoftReference wrf=new SoftReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());
2.1.1.4弱引用
弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象。在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。
Object obj=new Object();
WeakReference wrf=new WeakReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());
2.1.1.5虚引用
也称虚引用,是通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样。无法通过虚引用访问对象的任何属性或者函数。 那就要问了要它有什么用? 虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。比如说这个对象被回收之后发一个系统通知啊啥的。
虚引用是必须配合ReferenceQueue 使用的,具体使用方法和上面提到软引用的一样。主要用来跟踪对象被垃圾回收的活动。
我们知道了Entry的key是弱引用,弱引用的作用是什么我们知道了,那么至于为什么要用弱引用,我们等下再回来看,先把整个流程搞清楚!
2.1.2 初始化方法
private T setInitialValue() {
//调用initialValue方法,默认为null,可以重写。重写设置初始值
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//根据线程获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//第一次初始化,为null
if (map != null)
map.set(this, value);
else
//走到创建Map逻辑 1.2.1
createMap(t, value);
return value;
}
初始化创建Map
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
进入ThreadLocalMap初始化构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue){
//初始化Entry数组
table = new Entry[INITIAL_CAPACITY];
//根据线程的hashcode取模得到我应该放入数据的哪个下标位置
int i = firstKey.threadLocalHashCode &(INITIAL_CAPACITY - 1);
//在计算出的下标位置,放入entry,key为ThreadLocal对象,value为初始化的值
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置ThreadLocalMap的threshold值,16*2/3=10
setThreshold(INITIAL_CAPACITY);
}
自此,初始化流程完成!!
代码注释自认为给的很详细了
2.2Set方法
2.2.1.set入口
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程的Map,这个时候,我们上面get的时候已经初始化,已经有值
ThreadLocalMap map = getMap(t);
//get如果在map之前执行,肯定不为null
if (map != null)
//进入set方法
map.set(this, value);
else
createMap(t, value);
}
set方法
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.
//将原有的table赋值给tab
Entry[] tab = table;
//得到tabl的大小
int len = tab.length;
//根据key的hashCode 取模数组,得到数据的下标,同一个key的时候,hashcode一样,所以
//根据key找到的下标已经有entry对象并且已经赋值了初始化的值
int i = key.threadLocalHashCode & (len-1);
//在同一个key的get.set之后,e不为null,进入for循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//得到entry的key
ThreadLocal<?> k = e.get();
//因为get set传入的threadlocal对象是一个,满足条件
if (k == key) {
//将entry对象的value更改为新的value,返回
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个是最基础的流程,我们大概可以整理流程图如下:
假如2个线程,操作integerThreadLocal这个ThreadLocal对象,并且ThreadLocal对象的hash值计算后在Entry中的数组下标为5,integerThreadLocal下标为3 栗子中的代码呦
正常流程如下: get方法后
Set方法后
多个线程,就是多个外面的Thread,做到线程之间数据隔离
看完了get、set,看一下remove吧
2.3remove方法
public void remove() {
//根据当前线程获取ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
//如果ThreadLocalMap!=null
if (m != null)
//移除
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//根据key得到在数组中的下标位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果下标位置Entry的key为当前对象,进行整理清楚
if (e.get() == key) {
//解除Entry对key的弱引用
e.clear();
//对下标进行清除,并且对table进行整理
expungeStaleEntry(i);
return;
}
}
}
顺便看下扩容逻辑吧
2.4 扩容逻辑
入口还是在set方法
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.
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) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//没有进行清理并且size大于等于我的扩容界限,调用rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
Rehash方法
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//当容量大于等于四分之三时,进入resize方法
if (size >= threshold - threshold / 4)
resize();
}
resize方法
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//扩容容量为原容量的2倍
int newLen = oldLen * 2;
//初始化数组长度
Entry[] newTab = new Entry[newLen];
int count = 0;
//循环遍历老的容量大小
for (int j = 0; j < oldLen; ++j) {
//遍历Enrty
Entry e = oldTab[j];
//如果Entry不为null
if (e != null) {
//获取Entry的key
ThreadLocal<?> k = e.get();
//如果key为null,无效数据,把value设置为空,让value能gc回收
if (k == null) {
e.value = null; // Help the GC
} else {
//不为空,得到k的新的下标地址
int h = k.threadLocalHashCode & (newLen - 1);
//如果!=null.代表发生hash冲突
while (newTab[h] != null)
//线性探测下一个
h = nextIndex(h, newLen);
//赋值给为空的entry位置
newTab[h] = e;
count++;
}
}
}
//设置下一次的扩容值
setThreshold(newLen);
size = count;
table = newTab;
}
2.5其他非正常情况
2.5.1当ThreadLocal与ThreadLocal1的hash值冲突
我们来看set方法中多线程中多个ThreadLocal的hashCode冲突时,怎么解决,我们回到set方法
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.
Entry[] tab = table;
int len = tab.length;
//这里可能发送hash冲突,假如threadLocal1跟threadLocal 2个对象的hash值相同,下标都是5
int i = key.threadLocalHashCode & (len-1);
//通过i去拿数据的Entry,我们拿到的是ThreadLocal的,因为
ThreadLocal占据了5这个位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//得到的是ThreadLocal对象
ThreadLocal<?> k = e.get();
//ThreadLocal !=ThreadLocal1
if (k == key) {
e.value = value;
return;
}
//第一个循环 k也不等于null
//第二轮循环,
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
//3个条件都不满足,进入下一个循环
//i= nextIndex(i, len) 去找下一个小标的位置,直到找到下一个key为空的为止,这个场景我们等下过
//或者遍历完到一个null的位置,就不在循序
}
//找到一个为null的位置(肯定有,因为有扩容机制)
tab[i] = new Entry(key, value);
int sz = ++size;
//清理后,如果超过我的扩容界限 扩容界限为三分之二,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
冲突取值 我们知道它是用的线性探测去解决hash的,那么会出现一个问题?我根据hash去拿到的对象,可能不再是我自己想要的对象!
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据threadLocal对象 去map中获取Entry,如果冲突了我们看下怎么拿
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
getEntry方法:
private Entry getEntry(ThreadLocal<?> key) {
//根据key的hash下标值去取值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果取到的enrty不为null 并且对象也是我需要的对象,直接返回
if (e != null && e.get() == key)
return e;
else
//如果不是我想要的对象,进入getEntryAfterMiss
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss方法:
//key:我需要get的对象 i 根据key计算出来的下标 e 下标中的当前值
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i,Entry e) {
Entry[] tab = table;
int len = tab.length;
//如果e!=null。进入逻辑,如果e为null,说明ThreadLocal没有设置value,直接返回空
while (e != null) {
//得到当前位置的Entry对象
ThreadLocal<?> k = e.get();
//如果当前位置的Entry跟传进来的一致,直接返回
if (k == key)
return e;
if (k == null)
//如果对象的key被GC回收,进入整理逻辑,把当前位置设置为null 并且进行整理,rehash
expungeStaleEntry(i);
else
//去下一个线性找
i = nextIndex(i, len);
//把e设置为下一个Enrty对象
e = tab[i];
}
return null;
}
2.5.2当Key被GC回收处理
我们刚才讲过我们的key是弱引用,何为弱引用,就是我这个key就算外面有引用,只要发生GC也会被回收,就会出现我Entry的数据有可能是key为null ,但是value不为null的场景。 我们继续来看ThreadLocal怎么解决,继续回到set方法
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.
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;
}
//因为Key被回收,所以key为null,会进入replaceStaleEntry方法
if (k == null) {
replaceStaleEntry(key, value, i);//这里这里
return;
}
}
//找到key为null的,不会走下面逻辑
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry方法: 看这个方法,我们举个例子:在Thread1线程执行threadLocal1.set10);同时threadLocal1通过hash算法得到的下标为5;然后5的下标的key被GC回收,key=null。
//key为我需要获取值的ThreadLocal对象,value为需要set的值 i为key被回收的数组下标
//根据举例的场景:key为ThreadLocal1对象 value=10 i=5
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
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).
//slotToExpunge为5
int slotToExpunge = staleSlot;
//向数组前面轮询找 找到一个null的entry为止 假如下标为4的entry为null,跳出循环
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//假如下标为4的不是null,并且是被GC回收的,那么slotToExpunge赋值为向前找,找到最靠近null的被GC回收的Entry
if (e.get() == null)slotToExpunge = i;
// Find either the key or trailing null slot of run,whichever
// occurs first
//向后循环,找到entry为null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
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.
//假如向后找到了key跟我传入的一样的entry
if (k == key) {
//如果一样,替换value
e.value = value;
//假如下标为7的跟我传入的key是一样的
tab[i] = tab[staleSlot];
//在下标为5的位置放入7下的entry
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果slotToExpunge=slotToExpunge,则向前遍历没有找到key被回收的Entry
if (slotToExpunge == staleSlot)
//将slotToExpunge改成7
slotToExpunge = i;
//执行cleanSomeSlots方法
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//expungeStaleEntry方法
//返回
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.
//如果循环到后面的也是被GC回收的,并且向前遍历没有找到key被回收的Entry
if (k == null && slotToExpunge == staleSlot)
//slotToExpunge设置为 key被GC回收的Entry的下标位置
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//回收的entry的value设置为null (利于value对象回收)
tab[staleSlot].value = null;
//在回收的下标位置,新建对象赋值
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//slotToExpunge!=staleSlot,需要向前或者向后有找到需要清理的Entry,执行cleanSomeSlots
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);//expungeStaleEntry方法
}
expungeStaleEntry方法:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//将传进来的下标位置的Entry value设置为null value就可以被GC回收了
//将传进来的下标位置的Entry设置为null 清理空间
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; //数组里的size-1
// Rehash until we encounter null
Entry e;
int i;
//根据传进来的位置向后遍历,遍历到null为止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果entry对象的key被GC回收,清空entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果entry的对象没有被GC回收
//重新去计算下这个位置下的key的hash
int h = k.threadLocalHashCode & (len - 1);
//如果占的位置不是它hash的位置
if (h != i) {
//把现在的位置设置为null
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
//看该有的位置是不是空的,如果不是,去找寻下一个null的(开放寻址解决hash冲突)
while (tab[h] != null)
h = nextIndex(h, len);
//放到该有的位置去
tab[h] = e;
}
}
}
//返回i的值 传进来的下标的 后面的最接近null的entry
return i;
}
cleanSomeSlots方法:
//i 传入下标 n为传进来的数组的长度
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//根据传进来的下标去找下标后一个
i = nextIndex(i, len);
//得到该下标的enrty
Entry e = tab[i];
//如果下标的enrty 的key被GC回收了
if (e != null && e.get() == null) {
//n改为table的长度
n = len;
removed = true;
//拿到i去清除与重新rehash后面的,直到找到null为止
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); //不用遍历n次,只遍历n/2次,达到时间与空间的平衡
return removed; //如果有清除,设置为true
}
前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下map不为空时的执行逻辑
- 根据key的散列哈希计算Entry的数组下标
- 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
- 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
- 如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据
- rehash扩容
3为什么Key要弱引用?
假如每个key都强引用指向ThreadLocal的对象,也就是上图虚线那里是个强引用,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,map也跟着回收。
如果key是强引用,那么当我们执行threadLocal=null时,这个对象还被key关联,无法进行回收,只有当线程结束后,才会取消关联
但是用弱引用,我们就能在GC的时候,回收!
但是如果用的是线程池,那么的话线程就不会结束,只会放在线程池中等待下一个任务,但是这个线程的 map 还是没有被回收,它里面存在value的强引用,所以会导致内存溢出。
所以一般用threadLocal.remove()来清除内存 在ThreadLocal的生命周期中,都存在这些引用。看下图:实线代表强引用,虚线代表弱引用。
4Value为什么不用弱引用
是因为不清楚这个Value 除了map 的引用还是否还存在其他引用,如果不存在其他引用,当GC 的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用
怎么解决hash冲突
1.首先,那个魔数就能保证重复性会低,但是基数必须是2的N次方(举例) 2.用开放寻址法,如果真的查到的下标已经存在数据,就去找下一个,找到一个null的为止,并且是环形查找,因为肯定会有空的,会进行提前扩容