目录
1.ThreadLoca的作用
ThreadLoca的作用:可以实现线程隔离
简单案例了解一下ThreadLoca
public class ThreadLocalDemo {
public static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
//初始化值
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(()->{
int num = local.get();
num += 5;
local.set(num);
System.out.println(Thread.currentThread().getName()+"num:" + local.get());
});
}
for (int i = 0; i < 5; i++) {
threads[i].start();
}
}
}
2.ThreadLocal源码
get方法
1.入口
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//去根据Thread拿ThreadLocalMap,
//1.1 我们发现Thread类下会有个数据 对象叫做ThreadLocalMap
ThreadLocalMap map = getMap(t);
//线程第一次进来,map肯定是null
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();
}
1.1 获取线程中的 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 的 key 是一个对于 ThreadLocal 这个对象的弱引用。
Java四大引用
我们知道了 Entry 的 key 是弱引用,弱引用的作用是什么我们知道了,那么 至于为什么要用弱引用,我们等下再回来看,先把整个流程搞清楚!
1.2 初始化方法
private T setInitialValue() {
//根据用户定义的initialValue方法,去拿到我们的初始值
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//去线程里面拿threadLocal,还是空的,进入createMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//走到创建Map逻辑 1.2.1
createMap(t, value);
return value;
}
1.2.1 初始化创建 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);
}
自此,初始化流程完成!!
set方法
1.set入口
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程的Map,这个时候,我们上面get的时候已经初始化,已经有值
ThreadLocalMap map = getMap(t);
//get如果在map之前执行,肯定不为null
if (map != null)
//进入set方法 1.1
map.set(this, value);
else
createMap(t, value);
}
1.1 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;
//没有进行清理并且size大于等于我的扩容界限,调用rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个是最基础的流程,我们大概可以整理流程图如下:
假如2 个线程,操作 integerThreadLocal 这个 ThreadLocal 对象,并且ThreadLocal对象的 hash 值计算后在 Entry 中的数组下标为 5 ,integerThreadLocal下标为 3
正常流程结构如下
多个线程,就是多个外面的Thread,做到线程之间数据隔离
扩容机制
扩容机制 方法入口在set方法里
//没有进行清理并且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;
}
Hash冲突时
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) {
@SuppressWarnings("unchecked")
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. 当 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;
//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;
//向后循环,找到entry为null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//假如向后找到了key跟我传入的一样的entry
if (k == key) {
//如果一样,替换value
e.value = value;
//假如下标为7的跟我传入的key是一样的
tab[i] = tab[staleSlot];
//在下标为5的位置放入7下的entry
tab[staleSlot] = e;
//如果slotToExpunge=slotToExpunge,
//则向前遍历没有 找到key被回收的Entry
if (slotToExpunge == staleSlot)
//将slotToExpunge改成7
slotToExpunge = i;
//执行cleanSomeSlots方法
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
//返回
return;
}
//如果循环到后面的也是被GC回收的,并且向前遍历没有找到key被回收的Entry
if (k == null && slotToExpunge == staleSlot)
//slotToExpunge设置为 key被GC回收的Entry的下标位置
slotToExpunge = i;
}
//回收的entry的value设置为null (利于value对象回收)
tab[staleSlot].value = null;
//在回收的下标位置,新建对象赋值
tab[staleSlot] = new Entry(key, value);
//slotToExpunge!=staleSlot,需要向前或者向后有找到需要清理的Entry,
//执行cleanSomeSlots
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
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);
}
//不用遍历n次,只遍历n/2次, 达到时间与空间的平衡
} while ( (n >>>= 1) != 0);
return removed; //如果有清除,设置为true
}
前面分析了 set 方法第一次初始化 ThreadLocalMap 的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下 map 不为空时的执行逻辑
- 根据key的散列哈希计算Entry的数组下标
- 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
- 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
- 如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据
- rehash扩容
3.ThreadLocal面试常问
ThreadLocal怎么解决hash冲突的?