ThreadLocal源码分析(JDK8)

时间:2022-11-17 17:33:21
ThreadLocal特性及使用场景:
1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);

ThreadLocal应注意:
1、ThreadLocal并未解决多线程访问共享对象的问题;
2、ThreadLocal并不是每个线程拷贝一个对象,而是直接new(新建)一个;
3、如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题。
1、ThreadLocal<T>初始化
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT= 0x61c88647;
/**
* The next hash code to be given out. Updated atomically. Starts at zero.
*/
// 源码说nextHashCode初始值为0,但实际调试时显示初始值为1253254570,费解?
// 而且当初始化完毕后,nextHashCode的值又变为0,说明其初始值确实是0的。
private static AtomicInteger nextHashCode = new AtomicInteger();
private static intnextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal类变量有3个,其中2个是静态变量(包括一个常量),实际作为作为ThreadLocal实例的变量只有threadLocalHashCode这1个,而且已经初始化就不可变了。
创建ThreadLocal实例时有哪些操作呢:
ThreadLocal初始化时会调用nextHashCode()方法初始化threadLocalHashCode,且threadLocalHashCode 初始化后不可变threadLocalHashCode可用来标记不同的ThreadLocal实例。


2、内部类
2.1 ThreadLocalMap
ThreadLocalMap是 定制的hashMap,仅用于维护当前线程的本地变量值。仅ThreadLocal类对其有操作权限,是Thread的私有属性。为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题,hash table的key是弱引用WeakReferences。当空间不足时,会清理未被引用的entry。
ThreadLocalMap中的重点:
static class Entryextends WeakReference<ThreadLocal<?>>{
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v){
super(k);
value= v;
}
}
Note:
ThreadLocalMap的key是ThreadLocal,value是Object(即我们所谓的“线程本地数据”)。

2.2 SuppliedThreadLocal<T> extends ThreadLocal<T>
SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null。
源码如下:
static final class SuppliedThreadLocal<T>extends ThreadLocal<T>{
private final Supplier<?extends T> supplier;
SuppliedThreadLocal(Supplier<?extends T> supplier){
this.supplier= Objects.requireNonNull(supplier);
}
@Override
protected T initialValue(){
return supplier.get();
}
}

3、主要方法
3.1、T get()
返回当前线程的value。
public T get(){
Thread t= Thread.currentThread(); // 获取当前线程
ThreadLocalMap map= getMap(t); // 获取当前线程对应的Map
if(map!=null){
ThreadLocalMap.Entry e = map.getEntry(this); // 详见3.1.1
if(e!=null){ // map不为空且当前线程有value,返回value
@SuppressWarnings("unchecked")
T result=(T)e.value;
return result;
}
}
return setInitialValue(); // 初始化再返回值
}
-----
getMap的源码:
ThreadLocalMap getMap(Thread t){
returnt.threadLocals;
}
getMap(t)返回当前线程的成员变量ThreadLocalMap(Thread的成员变量有ThreadLocalMap,这一点可以查看Thread的源码,如下)很明确的说明了 ThreadLocal属于线程,ThreadLocalMap由ThreadLocal持有,说到底,ThreadLocalMap 也是线程所持有。每个线程Thread都有自己的ThreadLocalMap。
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

--------
setInitialValue源码:
private T setInitialValue(){
T value= initialValue(); //调用重写的initialValue,返回新值
Thread t= Thread.currentThread();
ThreadLocalMap map= getMap(t);
if(map!=null) // 当前线程的ThreadLocalMap不为空,则直接赋值
map.set(this, value);
else
// 为当前线程创造一个ThreadLocalMap(this, firstValue)并赋初值,this为当前线程
createMap(t, value);
return value;
}
protected T initialValue() {
return T; // 自定义返回值
};
createMap源码:
void createMap(Thread t, T firstValue){
t.threadLocals=new ThreadLocalMap(this, firstValue);
}
ThreadLocal之get流程:
1、获取当前线程t;
2、返回当前线程t的成员变量ThreadLocalMap(以下简写map);
3、map不为null,则获取以当前线程为key的ThreadLocalMap的Entry(以下简写e),如果e不为null,则直接返回该Entry的value;
4、如果map为null或者e为null,返回setInitialValue()的值。setInitialValue()调用重写的 initialValue()返回新值(如果没有重写initialValue将返回默认值null ),并将新值存入当前线程的ThreadLocalMap(如果当前线程没有ThreadLocalMap,会先创建一个)。


3.2、void set(T value)
为【当前线程】的【当前ThreadLocal】赋值(初始值or新值)。和setInitialValue相当相似,就不多分析了。
public void set(T value){
Thread t= Thread.currentThread();
ThreadLocalMap map= getMap(t);
if(map!=null)
map.set(this, value);
else
createMap(t, value);
}

3.3、void remove()
获取当前线程的ThreadLocalMap,map不为空,则移除当前ThreadLocal作为key的键值对。
public void remove(){
ThreadLocalMap m= getMap(Thread.currentThread());
if(m!=null)
m.remove(this);
}
Note:
remove()移除当前线程的当前ThreadLocal数据(只是清空该key-value键值对),而且是 立即移除,移除后,再调用get方法将重新调用initialValue方法初始化(除非在此期间调用了set方法赋值)。


3.4、static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
JDK8新增,支持Lambda表达式,和 ThreadLocal重写的initialValue()效果一样。
public static<S> ThreadLocal<S> withInitial(Supplier<?extends S> supplier){
return new SuppliedThreadLocal<>(supplier);
}
可以看出,withInitial()方法的入参是函数式接口Supplier,返回值是JDK8新增的内部类SuppliedThreadLocal,正如2.2所说,区别仅在于支持Lambda表达式赋值而已。使用事例如下:
@Test
public void jdk8Test(){
Supplier<String> supplier =new Supplier<String>(){
@Override
public String get(){
return"supplier_new";
}
};
threadLocal= ThreadLocal.withInitial(supplier);
System.out.println(threadLocal.get());// supplier_new
threadLocal= ThreadLocal.withInitial(()->"sup_new_2");
System.out.println(threadLocal.get());// sup_new_2
ThreadLocal<DateFormat> localDate = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));
System.out.println(localDate.get().format(new Date()));// 2017-01-22
ThreadLocal<String> local =new ThreadLocal<>().withInitial(supplier);
System.out.println(local.get());// supplier_new
}
Note:
·withInitial(supplier)是有返回值ThreadLocal的,So实例化时需将其赋值给ThreadLocal实例。

4、图解 ThreadLocal
每个线程可能有多个ThreadLocal,同一线程的各个ThreadLocal存放于同一个ThreadLocalMap中。
ThreadLocal源码分析(JDK8)
ThreadLocal源码分析(JDK8)
图解ThreadLocal(JDK8).vsdx原图下载地址: https://github.com/zxiaofan/JDK-Study/tree/master/src/java1/lang/threadLocal


5、 ThreadLocal-ThreadLocalMap源码分析

5.1、Entry getEntry(ThreadLocal<?> key)
首先来看get方法,你会发现ThreadLocalMap的get方法和传统Map不同,其返回的不是key-value的value,而是整个entry,当时entry的key是ThreadLocal,value是存放的值,这点是一致的。
a、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;
else
return getEntryAfterMiss(key, i, e);
}
getEnrty方法只会处理key被直接命中的entry,没有直接命中的(key冲突的)数据将调用getEntryAfterMiss()方法返回对应enrty,按照源码解释,这样做是为了尽可能提升直接命中的性能。
ThreadLocalMap之getEntry的流程:
1、计算Entry数组的index((length - 1) & key.hash)。
索引计算和HashMap的异同:
①相似之处:计算方式相同,均为(length - 1) & key.hash;length均为底层结构的大小(是大小,不是实际size)。
②不同之处:HashMap(JDK8)底层数据结构是 位桶+链表/红黑树,而ThreadLocalMap底层数据结构是 Entry数组;HashMap的key.hash的计算方式是 native、异或、无符号位移,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),ThreadLocalMap的key.hash从ThreadLocal实例化时便由nextHashCode()确定。
2、获取对应index的节点Entry;
3、如果返回节点entry 有值且其key未冲突(只有1个即entry返回的key等于传入的key),则直接返回该entry;
4、返回entry为空或键冲突,则调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法返回entry。

b、getEntryAfterMiss源码分析:
getEntryAfterMiss处理那些getEntry时没有被命中的key(value为空的直接返回null,so更确切的说是命中且有冲突的key)。入参是当前ThreadLocal,key在数组的索引index,以及index对应的键值对。
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);
else
i= nextIndex(i, len);
e= tab[i];
}
return null;
}
ThreadLocalMap之getEntryAfterMiss的流程:
仅分析Entry不为空的情况,
1、获取entry的key;
2、如果key一致(内存地址=判断),则返回该entry;
3、如果key为null,则调用expungeStaleEntry方法 擦除该entry;
4、其他情况则通过nextIndex方法获取下一个索引位置index;
5、获取新index处的entry,再死循环2/3/4,直到定位到该key返回entry或者返回null。
private static int nextIndex(int i,int len){
return((i+1< len)? i+1:0); // 把索引加1即可
}
c、expungeStaleEntry源码分析:
只要key为null均会被擦除,使得对应value没有被引用,方便回收。
private int expungeStaleEntry(int staleSlot){
Entry[] tab = table;
int len= tab.length;
// expunge entry at staleSlot
tab[staleSlot].value=null; // 擦除当前index处value
tab[staleSlot]=null; // 擦除当前index处key
size--;
// Rehash until we encounter null
Entry e;
int i;
for(i= nextIndex(staleSlot, len); // 计算下一个index
(e= tab[i])!=null; // 新index处entry不为空
i= nextIndex(i, len)){ // 计算下一个index
ThreadLocal<?> k = e.get(); // 获取新key(ThreadLocal)
if(k==null){ // key为null,再次置空
e.value=null;
tab[i]=null;
size--;
}else{
int h= k.threadLocalHashCode&(len-1); // 计算新index
if(h!= i){ // index若未变化,说明没有多余的entry了
tab[i]=null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// 一直扫到最后一个非空位置,将其值置为碰撞处第一个entry。
while(tab[h]!=null)
h= nextIndex(h, len);
tab[h]= e;
}
}
}
return i;
}


5.2、set(ThreadLocal<?> key, Object value)
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)]){
// 当前index处已有entry
ThreadLocal<?> k = e.get();
if(k== key){ // key(ThreadLocal)相同,更新value
e.value= value;
return;
}
if(k==null){ // 出现过期数据
// 遍历清洗过期数据并在index处插入新数据,其他数据后移
replaceStaleEntry(key, value, i);
return;
}
}
tab[i]=new Entry(key, value);
int sz=++size;
// 没有过期数据被清理且实际size超过扩容阈值
if(!cleanSomeSlots(i, sz)&& sz>= threshold)
rehash();
}

rehash():
size:table的实际entry数量;扩容阈值threshold:table.lenrth(默认16)大小的2/3;
首先调用expungeStaleEntries删除所有过期数据,如果清理数据后size>=threshold的3/4,则2倍扩容。
ps:阈yù值又叫临界值,是指一个效应能够产生的最低值或最高值。阀fá 控制、开关、把持。

ThreadLocalMap和HashMap在hash冲突时的解决方案对比:
HashMap:若冲突则将新数据按链表或红黑树逻辑插入。
put(K key, V value)的逻辑:
1、判断键值对数组tab[]是否为空或为null,是则resize(); 
2、根据键值key的hashCode()计算hash值得到当前Node的索引i( bucketIndex ),如果tab[i]==null【没碰撞】,直接新建节点添加,否则【碰撞】转入3 
3、判断当前数组中处理hash冲突的方式为红黑树还是链表(check第一个节点类型即可),分别处理。【①是红黑树则按红黑树逻辑插入;②是链表,则遍历链表,看是否有key相同的节点;③有则更新value值,没有则新建节点,此时若 链表数量大于阀值8【9个】,则调用treeifyBin方法(此方法先判断table是否为null或tab.length小于64,是则执行resize操作,否则才将链表改为红黑树)。】
4、如果size+1> threshold则resize。

ThreadLocalMap:
1、若指定位置index已有数据entry,逐个遍历entry:
1.1、若index处key相同,则更新value;
1.2、若index处key为null,则调用replaceStaleEntry清理过期数据并插入新数据(从index处挨个遍历,直到找到相同key更新value结束,或者一直未找到,则在index处放入new Entry)。replaceStaleEntry遍历时会将entry逐个后移,也就是说set进去的最新entry一定会放在index处,方便get时直接命中。
2、index处无数据,则放入新entry;随后清理过期数据并判断是否2倍扩容(size>=threshold的3/4)。

参考资料:
http://www.cnblogs.com/dolphin0520/p/3920407.html
有任何问题,欢迎指正探讨。
欢迎个人转载,但须在文章页面明显位置给出原文连接;
未经作者同意必须保留此段声明、不得随意修改原文、不得用于商业用途,否则保留追究法律责任的权利。

【 CSDN 】:csdn.zxiaofan.com
【GitHub】:github.zxiaofan.com

如有任何问题,欢迎留言。祝君好运!
Life is all about choices!
将来的你一定会感激现在拼命的自己!