JDK源码学习03-ThreadLocal源码简析
在读完源码之后值得注意的细节:
- 在get方法中,先获取map,如果map不为空才就从map中取,如果map为空,就返回initialValue()
- 每个线程使用完ThreadLocal之后,需要调用ThreadLocal的remove方法
ThreadLocal简介
多个线程可能都需要某些相同的属性,我们可以在集成Thead类或者实现Runnable的时候,给类赋予私有属性,但是这样做比较呆板,于是ThreadLocal就出现了。比如在Java的数据库连接池中,多个线程会被分别分配一个connection对象,而一个线程必然从始至终都是只使用一个connection对象的(我们可以想想为什么?比如两个线程使用一个connection对象对数据库进行事务操作,就乱套了)。此时我们希望可以给每个线程设置自己的专属属性,就使用ThreadLocal来实现。
但需要注意的是,ThreadLocal会出现内存泄漏的情况,下面也会详细结合JVM的GC讲解。
ThreadLocal其实原理很简单,主要就是在Thread类中本来就拥有一个属性,这个属性就专属于这个线程。学习Java时间久了一点,发现Java确实一切皆对象,Class是对象,Void是对象,线程Thread肯定也是对象。
既然ThreadLocalMap是每个线程都有的私有对象,说明该对象不存在线程竞争问题,并且生命周期从创建(使用ThreadLocal的set或者withInitial会初始化这个map对象),如果不手动删除的话,会一直到线程结束才停止。
比如:
我们把每个线程想象成一个人,而
十个人竞争一个共享单车,这是不安全的(并且每个人没有自己的独特性)。但是线程的属性 ThreadLocal.ThreadLocalMap threadLocals = null;的出现相当于给每个人都配备了一个停车位,可以允许每个人拥有自行车。而ThreadLocal的静态方法withInitial则实现了给threadLocals赋值初始值,相当于给每个人的车位都配一辆基础版自行车,这样每个人用自己的自行车就可以了。并且每个人都可以对自己的自行车进行改装,并不会影响别人的自行车。就算没有使用静态方法withInitial,就算大家都没有被分配基础版自行车,但还是有停车位,可以通过ThreadLocal的set方法,给自己购买一辆自行车。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
看下面的代码,我们创建了一个ThreadLocal threadLocal对象,并且被两个线程先后访问。第一个线程修改了threadLocal的值,第二个线程再次访问的时候,发现获得的还是初始值。这是因为ThreadLocal给每个线程都初始化了一个ThreadLocalMap,里面的key是ThreadLocal对象,而value是学生对象,所以尽管线程A先于线程B访问并修改这个对象,但其实修改的是线程ThreadA.ThreadLocalMap中的kv对threadLocal->student中的student对象的值。而线程二读取的是ThreadB.ThreadLocalMap中的kv对threadLocal->student中的student对象的值。
static ThreadLocal<Student> threadLocal = ThreadLocal.withInitial(()->new Student("minxu"));
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread()+"开始干活。。。");
Student student = threadLocal.get();
student.setName("heiheihei");
System.out.println(student);
},"A").start();
new Thread(()->{
sleep(1000);
System.out.println( Thread.currentThread()+"开始干活。。。");
Student student = threadLocal.get();
System.out.println(Thread.currentThread()+" "+student);
},"B").start();
sleep(1500);
}
public static void sleep(int time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
输出:
Thread[A,5,main]开始干活。。。
Student{name=‘heiheihei’}
Thread[B,5,main]开始干活。。。
Thread[B,5,main] Student{name=‘minxu’}
下面我们来讲讲实现的过程和代码
然后以ThreadLocal作为key,将数据存储进入每个线程中,这样就不存在进程关系。

简单使用实例
源码查看
核心代码很简单,只有下面两三个函数
有手就能看懂,,,
// 本函数保证了只初始化一次和懒加载
public T get() {
Thread t = Thread.currentThread();
//ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocal内存泄漏原理
如果是强引用(实际是弱引用) --ThreadLocalMap中的key指向了ThreadLocal对象
我们知道JVM的gc只会对一个跟不可达的对象进行一次fanalize检查一次gc回收。
当线程Thread运行的时候,线程的属性ThreadLocal.ThreadLocalMap threadLocals是不会回收的,
不仅ThreadLocal的引用指向了ThreadLocal对象,而ThreadLocalMap中的key指向了ThreadLocal对象,
(我们可以先假设key-》ThreadLocal是强引用,为了改进此问题,实际使用的是弱引用)。所以即使ThreadLocal使用完毕
即ThreadLocal的引用不再指向了ThreadLocal对象,,由于还有key指向ThreadLocal对象,也没有办法回收ThreadLocal,
只有等Thread运行完才能回收。Thread的生命周期是很长的,所以这就叫做内存泄漏,无用对象无法回收。

实际是弱引用 –还是存在问题
我们将ThreadLocalMap中的key指向了ThreadLocal对象的引用改成弱引用
这样在栈中的强引用挂了之后,gc扫描到ThreadLocal对象就会把ThreadLocal对象回收了
这样看起来是不是没问题了?但是我们忽略了一件事,就是key虽然被回收了,但是value指向的对象没有被回收!!!! 也就是说ThreadLocalMap中的value如果不手动删除 如果线程生命很长,就会产生内存泄漏问题。