一、ThreadLocal的基本定义
官方定义:当使用 ThreadLocal 维护(set)变量时,ThreadLocal 为每个使用该变量的线程提供(get)独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 并不是一个 Thread,而是 线程Thread的一个局部变量。
什么是变量?从字面意思就知道是“会变化的数据”。ThreadLocal是线程变量,也就意味着它专门用于线程Thread对象,且在不同的线程对象里面代表着不同的值。
所以,ThreadLocal翻译为线程局部变量,更为贴切。
线程映射值,那么其实可以把它视为键值对Map。实际上,在ThrealLocal内部用的就是Map。
线程任务执行完毕后,线程对应的局部变量会被GC回收。
平时我们经常用到ThreadLocal 的方法是 get()、set()、remove()、initValue()。
下面模拟一个简单的ThreadLocal
import java.util.*;
/**
* Created by jay.zhou on 2018/9/7.
*/
public class MyThreadLocal<T> {
//内部采用Map<Thread,T>实现,让Thread 映射 线程局部变量
private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<>());
public T get() {
//创建值的引用
T value;
//获取当前线程
Thread currentThread = Thread.currentThread();
//先从map中获取值
value = map.get(currentThread);
//如果值是空,说明是第一次调用此方法,将其放入我们的键值对中
if (value == null) {
//获取定义的初始值
value = initValue();
//存入内部键值对中
map.put(currentThread, value);
}
return value;
}
public void set(T value) {
//获取当前线程
Thread currentThread = Thread.currentThread();
//存入内部键值对中
map.put(currentThread, value);
}
//移除此线程局部变量当前线程的值
public void remove() {
//获取当前线程
Thread currentThread = Thread.currentThread();
//移除键值对
map.remove(currentThread);
}
//初始值,可以被子类重写的方法
public T initValue() {
return null;
}
public static void main(String[] args) throws InterruptedException {
MyThreadLocal<List<String>> threadLocal = new MyThreadLocal<>();
//线程标准写法
new Thread(() -> {
List<String> list = Arrays.asList("a", "b", "c");
threadLocal.set(list);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
//分割线
System.out.println("--------");
}).start();
Thread.sleep(100);
new Thread(() -> {
List<String> list = Arrays.asList("d", "e", "f");
threadLocal.set(list);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
}).start();
}
}
/**
Thread-0
a
b
c
------------
Thread-1
d
e
f
*
/
二、ThreadLocal的源代码探索
在学习了ThreadLocal类的源码以后,我才发现,原来ThreadLocal没有上面那么简单。
首先,既然是线程的局部变量,那么肯定是存放在线程Thread里面的。
查看Thread类的源代码可以找到存放局部变量的代码,其实就是它的内部Map集合ThreadLocalMap。
public
class Thread implements Runnable {
//省略其它代码
ThreadLocal.ThreadLocalMap threadLocals = null;
}
原来,在Thread类中,持有了一个ThreadLocalMap集合。这个ThreadLocalMap集合的内部,是通过Entry这个内部类对象来存储数据的。这与HashMap和LinkedList实现原理十分相似:用对象来封装值。
Entry对象的构造函数在下面展示出来了。接收的是ThreadLocal与Object value。因此,Entry可以视为真正存储数据的地方。一个ThreadLocalMap里面有多个Entry,因为某个线程不可能只有一个变量吧。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 当前ThreadLocal对应的值 */
Object value;
//通过构造函数接收键值对 ThreadLocal--value,存储到具体的Entry对象中
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
综上,Thread、ThreadLocalMap、Entry(或者说ThreadLocal : Value键值对)的关系就很简单了。
一个Thread对象中持有一个的ThreadLocalMap。ThreadLocalMap中持有多个Entry键值对。键为具体的ThreadLocal对象local,值为local.set( Object value)的这个value对象。
在某个线程的任务代码中,调用ThreadLocal的set方法的时候,是怎么跟当前线程联系起来的呢?
public class ThreadLocal<T> {
public void set(T value) {
//获取当前线程,原来是通过这种方式与当前线程取得联系
Thread t = Thread.currentThread();
//获取到当前线程对象的ThreadLocalMap对象,因为ThreadLocalMap对象是默认的访问权限,在同包里是可以调用到的
ThreadLocalMap map = t.threadLocals;
//如果map不为空,说明之前调用过此set()方法,所以创建了ThreadLocalMap对象。
//没调用的话,那么就新创建ThreadLocalMap对象
if (map != null)
//把当前的threadLocal与value组成键值对Entry,放入ThreadLocalMap中
//此map.set(..)方法设计到内存回收,回收key为null的value的内存
map.set(this, value);
else
//看,如果是第一次调用set方法,那么会创建ThreadLocalMap对象。
createMap(t, value);
}
//创建ThreadLocalMap的逻辑非常简单,就是把新创建的ThreadLocalMap与当前线程t联系起来。
void createMap(Thread t, T firstValue) {
//总会调用这一步的,因为总有第一次调用ThreadLocal.set()方法。
//所以,把新创建的ThreadLocalMap绑定到当前线程 currentThread.threadLocals 变量上
//这里的键值对 this代表threadLocal对象,值就是set(firstValue)方法里面的参数firstValue
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
现在,某个线程Thread的任务里面,已经执行了threadLocal.set(value); 了。那么,怎么才能取出来值呢。
我们调用的是Object value = threadLocal.get(); 是用来获取值的代码。调用5秒钟,分析两小时。
我们知道,哪个线程执行了这个代码,哪个线程就是所谓的“当前线程”。
(1)先拿到当前线程
(2)再获取当前线程中的 ThreadLocalMap对象
(3)通过当前ThreadLocal 对象 ,在 ThreadLocalMap对象中去找 ,找到那组键值对 Entry对象《ThreadLoacl,Value》
(4)最终通过 Entry.Value ,返回我们要的值。
public class ThreadLocal<T>{
public T get() {
//因为值都存放到当前线程里面了,所以要去当前线程里面拿嘛
Thread t = Thread.currentThread();
//获取到当前线程对象的ThreadLocalMap对象
ThreadLocalMap map = t.threadLocals;
//如果map不为空,说明之前当前线程的任务代码里面之前调用过ThreadLocal的set()方法
if (map != null) {
//Entry是个键值对,键是ThreadLocal。通过ThreadLocal对象肯定能拿到那个键值对对象Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
//如果有值,那么就返回这个值。没值,那就返回初始值咯
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果在set() 方法调用之前就调用get(),那么只好返回 initValue()方法的值咯
return setInitialValue();
}
public void set(T value) {
//获取当前线程,原来是通过这种方式与当前线程取得联系
Thread t = Thread.currentThread();
//获取到当前线程对象的ThreadLocalMap对象,因为ThreadLocalMap对象是默认的访问权限,在同包里是可以调用到的
ThreadLocalMap map = t.threadLocals;
//如果map不为空,说明之前调用过此set()方法,所以创建了ThreadLocalMap对象。
//没调用的话,那么就新创建ThreadLocalMap对象
if (map != null)
//把当前的threadLocal与value组成键值对Entry,放入ThreadLocalMap中
map.set(this, value);
else
//看,如果是第一次调用set方法,那么会创建ThreadLocalMap对象。
createMap(t, value);
}
//创建ThreadLocalMap的逻辑非常简单,就是把新创建的ThreadLocalMap与当前线程t联系起来。
void createMap(Thread t, T firstValue) {
//总会调用这一步的,因为总有第一次调用ThreadLocal.set()方法。
//所以,把新创建的ThreadLocalMap绑定到当前线程 currentThread.threadLocals 变量上
//这里的键值对 this代表threadLocal对象,值就是set(firstValue)方法里面的参数firstValue
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
三、ThreadLocal导致的内存溢出
我们知道,使用线程池,关闭线程变成向线程池归还线程。所以,线程是一直存活的。
ThreadLocal存放的值,会放到线程的ThreadLocalMap中。如果每次任务都往内部存储一个线程局部变量,且这个变量可能一个大对象,次数多了以后,可能会造成内存溢出。
当然了,正常情况下,在线程被关闭以后,它的内部的ThreadLocalMap对象也一同被GC回收,再内部的Entry对象也被回收了,所以不使用线程池的情况几乎不可能造成内存溢出。
四、结论
(1)在某个具体的线程任务中,调用ThreadLocal.set(Object object)方法,其实就是用Entry封装数据。new Entry(threadLocal , object)。然后把这个Entry对象存放到 这个线程的 thread的ThreadLocalMap中。因此,可以说,ThreadLocal 只是操作 Thread 中的 ThreadLocalMap 对象的集合。 通过set存储数据到集合中,通过get从集合中获取数据。
(2)线程中的 ThreadLocalMap 变量的值是在 ThreadLocal 对象进行 set 或者 get 操作时创建的,原本是在Thread对象中是null。准确的说是,第一次调用ThreadLocal.set()方法,将会为当前线程创建ThreadLocalMap集合。
(3)当前线程Thread对象的ThreadLocalMap 的键,其实就是 ThreadLocal对象。
(4)当ThreadLocalMap内部的Entry个数超过阙值的2/3,map开始扩容并且清理部分Entry的内存。
(5)其实,之所以各个线程之间不会出现干扰的原因就是,在线程内部创建了一个新的Entry对象,以“空间换时间”。
.