一、什么是ThreadLocal变量
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
二、原理图
简单点理解就是,每个线程里都会有一个ThreadLocalMap集合,每次set(value)的时候都是将ThreadLocal变量的副(实际上是对ThreadLocal变量的弱引用,如上图的虚线)本作为key,value作为值,然后保存到线程里的map集合里。每次get()都是从当前线程内部的map里取。
三、源代码剖析
1、测试案例
public class ThreadLocalDemo {
static ThreadLocal<Person> threadLocal1 = new ThreadLocal<Person>();
public static void main(String[] args) {
Thread threadA = new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 沉睡两秒,取得threadLocal1里设置的值
System.out.println("======>>>>>threadA: " + threadLocal1.get());
});
Thread threadB = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 沉睡1秒,然后往threadLocal1里设置值
threadLocal1.set(new Person());
// 本线程里取threadLocal1里设置值
System.out.println("======>>>>>threadB: " + threadLocal1.get());
});
// 开启两个线程
threadA.start();
threadB.start();
}
}
说明: 在上面的小程序中,定义了一个ThreadLocal
变量,开启了两个线程,B线程使用set方法,threadLocal1.set(new Person());
向线程内部设置了一个值,A线程等待两秒后去获取这个值。
结果如下:
======>>>>>threadB: com.wp.face.thread.entity.Person@5ac42552
======>>>>>threadA: null
可以看到A线程并没有获取到B线程设置进去的值,这是怎么回事呢?
2、结果分析
a)、 其实是在B线程中使用ThreadLocal变量threadLocal1调set(new Person())
·方法的时候,是将threadLocal1变量的副本(其实是弱引用)和new出来的person对象作为key–value键值对,存放进了B线程内部的Map集合中了,由于是存放在B线程的内部,即是B线程私有的,所以A线程不可能读取到B线程内部的值。
b)、 当A线程使用threadLocal1.get()
方法去获取值时,其实是在A线程内部的map集合中查找,这样当然找不到了
2、set()方法如何保存值
1、set方法源码:
public void set(T value) {
// 1、获取到当前线程,即调用set方法的那个线程
Thread t = Thread.currentThread();
// 2、获取当前线程里面的map集合(map是当前线程的一个属性,源码如下)
ThreadLocalMap map = getMap(t); // ---(1)
if (map != null)
// 3、将threadLoacl对象的弱引用和value保存到当前线程的map集合里面去
map.set(this, value); // ----(2)
else
// 4、如果获取不到就根据当前线程和传入的value值创建一个map
createMap(t, value);
}
2、getMap(t)源码:
// 看看在(1)中是如何拿到当前线程的map的
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap getMap(Thread t) {
/**
* 很明显,threadLocals是当前线程的一个属性,
* 即每一个线程都有一个threadLocals变量
*/
return t.threadLocals;
}
3、get()方法如何获取值
1、get()方法源码
public T get() {
// 1、获取到当前线程,即调用get()方法的那个线程
Thread t = Thread.currentThread();
// 2、 通过当前线程t去获取当前线程里的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3、通过threadlocal对象去当前线程的map集合里获取值返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
四、拓展(主要是看弱引用)
set(this, value)源码:
说明:这里的this就是对threadlocal的一个弱引用。将对threadlocal的弱引用作为key,value作为值,保存进线程内部的map里
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;
}
}
// 将我们的key和value做成以一个entry,然后保存在map中(一个key-value键值对
// 就相当于一个entry),然后再跟踪到Entry里面去,看看entry是个什么东西(这块涉及到弱引用)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
entry源码:
// 可以看到Entry继承了弱引用WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 将我们的key(即指向threadlocal的引用)设置为虚引用
super(k);
value = v;
}
}
原理图:
在线程中使用set(value)
方法保存值到当前线程中时需要注意,如果ThreadLocalMap集合里的某些元素不使用了,那么我们需要手动的调用threadlocal.remove()
方法,来将那些不再使用的数据回收掉。
比如:
当上图的
ThreadLocal
变量的强引用没了,那么ThreadLocal
就可以被回收了,其他线程对他的弱引用key也会变为空,但是在线程内部仍然保留着对ThreadLocal
变量的弱引用key和value不会被回收,这时候就可能造成了内存泄漏。