一、ThreadLocal
1、ThreadLocal是线程内部一个成员变量,它只属于当前线程,对其他线程不可见。
2、线程维护的是ThreadLocal的副本变量,每个线程操作自己的副本变量互不影响,所以不用考虑线程安全性问题。
3、简单的理解就是同一个ThreadLocal在不同的线程里调用get()方法得到的value不一样。
4、它实现的是同一个线程间的数据共享(隔离的是线程);
锁实现的是不同线程间的数据共享
二、应用场景
一个我们接触多,容易理解,最可能用到的场景:
定义一个成员变量ThreadLocal,方法间调用不再传递业务参数,直接把参数set()到ThreadLocal里,到后面无论哪个方法里用到了可以直接get()获取.比如我们想统计各个接口的处理时间,把最开始的时间戳set到hreadLocal里,后面每个方法结束都可以get出来计算,在重新set新值,这样不用把时间当作参数传递了。它维护的副本变量,每个线程都有属于自己的变量,所以不会存在并发数据安全性问题。
三、原理
了解它的实现原理首先要清楚几个类以及他们之间得关系
Thread:线程,我们接触最多,跑我们的业务逻辑
ThreadLocal:一个跟Thread同级别的类,但它的内部类ThreadLocalMap是Thread的成员变量,Thread Local是联系Thread和ThreadLocalMap的桥梁,维护的是数据应用和数据存储。
ThreadLocalMap:是ThreadLocal的静态内部类,一个Thread对应一个ThreadLocalMap,它是为该线程存储数据的一种数据结构。它底层是一个Entry类型的数组实现
Entry:是ThreadLocalMap的内部类,他是由一组键值对组成,key就是ThreadLocal,它是一种弱引用,容易被回收;value就是我们要真正存储的数据。
他们之间的关系
一个Thread类对应一个ThreadLocalMap,ThreadLocalMap存数据是用Entry类型的数组实现的,Entry的键是ThreadLocal,值是要存的数据。所以一个ThreadLocalMap里可以存放多个ThreadLocal,每一个ThreadLocal对应该线程下的一种要存储的数据类型(如:int、String等)。
可以这么理解:Thread与ThreadLocalMap是一对一关系,他们与ThreadLocal是一对多关系.。
理解上面的关系后原理就很好总结了:ThreadLocal底层是一个类似于map的内部类ThreadLocalMap实现的,真正存储结构的是ThreadLocalMap,并且它与线程紧密关联,ThreadLocal作为一个桥梁维护Thread和ThreadLocalMap,负责操作ThreadLocalMap。ThreadLocalMap底层是Entry实现的,内部是一个Entry数组,这个Entry是由key和value组成,key就是ThreadLocal,value可以是任意对象,它继承了WeakReference,是个弱引用,所以key容易被回收,造成内存泄漏。
四、内存泄漏
ThreadLoca使用不当容易引起内存泄漏。何谓内存泄漏?Entry是键值对的形式存在,它继承了WeakRefencer<ThreadLocal<?>>是一种弱引用,它的key容易被GC回收,但值value是一直存在的,没有被回收,这就导致可能存在key是null,value有值的情况,当大量这种对象存在时,内存容易被占满。
解决方法:我们在使用完ThreadLocal后手动remove()一下,清空无效的value值
JDK的解决方案:在调set()、get()方法时内部都会遍历查找无效的value进行清空
五、用法
开发主要使用到的三个方法set()、get()、remove()
public class ThreadLocalTest {
static ThreadLocal<Long> threadLocalLong = new ThreadLocal<Long>();
static ThreadLocal<String> threadLocalString = new ThreadLocal<String>();
public static void main(String[] args) {
for(int i=0;i<5;i++){
final int sum=i;
new Thread(()->{
long l = System.currentTimeMillis();
threadLocalLong.set(l);
threadLocalString.set(Thread.currentThread().getName());
System.out.println(threadLocalString.get()+"=初次set值="+ threadLocalLong.get());
sleep(sum);
System.out.println(threadLocalString.get()+"=再次set的值="+ threadLocalLong.get()+"=两次差值="+(threadLocalLong.get()-l));
threadLocalLong.remove();
threadLocalString.remove();
},"线程-"+i).start();
}
}
public static void sleep(int i) {
try {
Thread.sleep(3000*i);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadLocalLong.set(System.currentTimeMillis());
}
}
六、源码
源码重点关注set就可以,get、remove用到的都在set里有
public void set(T value) {
Thread t = Thread.currentThread();
//拿到只属于这个线程的私有变量map,说是map其实就是一个entry,里面就是我们要放的数据,类似于数组
//map里面可能存在多种类型的数据,但这些数据都是属于这个线程的。对应使用就是定义多个ThrealLocal,但map就一个
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null){
map.set(this, value);
}else{
//map为null,构建一个新得map,指向当前线程的ThreadLocal引用,
//这个线程就有了自己专属的ThradLocalMap,并且这个map里存放了要set的值,这个值还以弱引用形式引用着这个线程
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, value);
}
}
getMap()没啥好说的,就是拿这个线程的ThreadLocalMap,重点在map.set()里,map.set(this, value)的实现
/**
* 主要是在Entry数组里循环找当前线程的Entry,找不到就创建一个,遍历过程中顺便查了下Entry的弱引用(当前线程的引用对象)有没有被回收
* 注意:
* 1、Entry数组里是该线程下的任意类型数据,对应我们new的ThreadLocL
* 2、内存泄漏:因为Entry是弱引用,如果应用被回收了,value也要清理,不然越堆越多,容易报内存泄漏
* 3、前面已经判断了map是否为null,为啥后面还可能为null?很可能是正好触发了垃圾回收
* 4、多个线程创建Entry往数组里放时,可能会触发数组的阀值,需要扩容
*
*/
private void set(ThreadLocal<?> key, Object value) {
//根据ThreadLocal的hashcode找到这个key可能所在的位置
ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//从这个数组里去找属于当前线程的Entery
for (ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {//如果是当前线程就给它赋值
e.value = value;
return;
}
//如果key是空了,i这个位置上已经没引用了,被GC回收了,
if (k == null) {
// 把key=null的Entry和当前key的Entry替换,并检查和清理其他空的Entry
replaceStaleEntry(key, value, i);
return;
}
}
//如果这个数组里还没有Entry,是一个空数组,则构建一个Entry放入数组
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
//如果数组容量达到阀值就要扩容了
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
}
private void replaceStaleEntry( ThreadLocal<?> key, Object value,int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
ThreadLocal.ThreadLocalMap.Entry e;
int slotToExpunge = staleSlot;
//从当前位置往前面找,看前面位置上有没有引用为null的,直到找到数组里第一个引用为null的位置
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null; i = prevIndex(i, len)) {
if (e.get() == null) {
slotToExpunge = i;
}
}
//从当前位置往后面找,
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果存在key是当前线程的Entry,就赋值
if (k == key) {
e.value = value;
//引用为null的Entry和当前线程的Entry交换位置(当前线程的Entry放到当前这个引用为null的位置,引用为null的Entry放到这个线程的位置上)
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//经过上述操作后i位置上Entry引用为null
if (slotToExpunge == staleSlot) {
//如果前面位置上没有null引用(没走第一个循环)把这个空位置给slotToExpunge
slotToExpunge = i;
}
//slotToExpunge值最终是一个null引用的Entry位置,结合下面的判断并且值只能是第一个null引用的位置
//清除引用为null的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot) {
slotToExpunge = i;
}
}
//再次都找一下,都找完了也没找到当前线程的引用位置,就新构建一个
tab[staleSlot].value = null;
tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
if (slotToExpunge != staleSlot) {
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
七、总结
本文主要讲了ThreadLocal是什么、应用场景以及原理、源码等。作为入门级了解,知道是什么?用在哪?怎么用就可以,深入了解(面试扯皮)重点关注原理、内存泄漏、源码实现部分。