JDK源码系列:ThreadLocal实现原理
大家好,在软件开发过程中,一般情况下方法之间调用时都是通过接口参数来传递数据的,但有一些公共参数(userId、token、orgId、roleId等)的传递就不能那么干了,在Java中一般用ThreadLocal 去解决这个问题,今天老吕来分析下ThreadLocal的源码。
一、ThreadLocal的本质
通过共享内存来传递数据。但它是如何做到多线程安全的呢?为什么数据就不会串呢?
二、图解ThreadLocal的实现原理
整个过程中参与方主要有这几个角色:
1、Thread
线程,每一个线程都会有一个私有的线程栈空间。
2、ThreadLocal
透明的做到了和线程绑定传递数据,实际上是一个操作 ThreadLocalMap的入口工具。
3、ThreadLocalMap
数组实现的Map。用来存储要传递的数据。
4、Entry
数组元素。
通过阅读源码,我画出了上面的图,可以看出:
1、要传递的数据实际上并没有在ThreadLocal中存储,而是在ThreadLocalMap中,ThreadLocalMap实际上位于Thread对象中。
上面的注释也说了,这个map被ThreadLocal class 维护。
2、ThreadLocal实际上只是一个操作 ThreadLocalMap的入口工具。
3、每个线程是如何准确路由到自己的map上的呢?
以当前线程Thread对象来路由找到自己的map的,所以不会有线程安全问题。
ThreadLocalMap map = Thread.currentThread().threadLocals;就这么一句话就路由过去了。
4、内存泄漏指的是哪块内存?怎么就泄漏了呢?
map是由Entry数组实现的,Entry对象的实现如下:可以看到 ThreadLocal对象放入了WeakReference对象里,这就是声明了一个弱引用对象(GC时如果对象只被弱引用引用则会被回收),那是不是有一个疑问,ThreadLocal被GC回收了,这个存储不就完蛋了吗?咋用呢?事实它也不会被轻易回收,因为在你声明的ThreadLocal对象的地方肯定还有一个强引用,只有那个强引用被释放后,再下次GC时,ThreadLocal对象才会被回收。
所以如果你声明的那个ThreadLocal对象强引用被释放了,但是线程并没有结束(考虑线程池情况),这时候就会出现一个现象,Entry对象里key为null,value不为null,但是value永远也用不了了,这就相当于内存泄漏了。
5、如何避免泄漏?
1)在一个线程每一次运行生命周期的起始端调用 threadLocal.set(o)方法来重置覆盖上次可能遗留的资源
2)在一个线程每一次运行生命周期的末端 调用 threadLocal.remove()方法是否绑定的资源
3)threadLocal一般声明为static类型的对象,避免频繁创建与释放
三、既然明白了ThreadLocal的原理,有没有考虑过自己手写一个工具实现类似功能?
//手撕一个线程绑定传递工具
public class MyMap<K,V> {
private Map<Thread,Map<K,V>> threadMap = new ConcurrentHashMap<>();
public void put(K key,V value){
Map map = threadMap.get(Thread.currentThread());
if (map==null) {
threadMap.put(Thread.currentThread(),new HashMap());
}
map = threadMap.get(Thread.currentThread());
map.put(key,value);
}
public V get(K key){
Map<K,V> map = threadMap.get(Thread.currentThread());
if (map==null) {
return null;
}
return map.get(key);
}
}
//简单测试一把
public class ThreadLocalClient2 {
static MyMap<String,String> map = new MyMap<String,String>();
public static void main(String[] args) {
ThreadFactory threadFactory = Executors.defaultThreadFactory();
threadFactory.newThread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 5; i++) {
map.put("name"+i,Thread.currentThread().getName()+"zhangsan"+i);
System.out.println(Thread.currentThread().getName()+":"+map.get("name"+i));
}
}
}).start();
threadFactory.newThread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 5; i++) {
map.put("name"+i,Thread.currentThread().getName()+"lishi"+i);
System.out.println(Thread.currentThread().getName()+":"+map.get("name"+i));
}
}
}).start();
while (true);
}
}
运行结果是正常的
pool-1-thread-1:pool-1-thread-1zhangsan0
pool-1-thread-2:pool-1-thread-2lishi0
pool-1-thread-1:pool-1-thread-1zhangsan1
pool-1-thread-2:pool-1-thread-2lishi1
pool-1-thread-2:pool-1-thread-2lishi2
pool-1-thread-1:pool-1-thread-1zhangsan2
pool-1-thread-1:pool-1-thread-1zhangsan3
pool-1-thread-2:pool-1-thread-2lishi3
pool-1-thread-1:pool-1-thread-1zhangsan4
pool-1-thread-2:pool-1-thread-2lishi4
有没有发现老吕实现的这个线程绑定传递工具类有什么缺陷???
很明显是有内存泄漏问题,线程结束后,map里的数据不能自动释放。
我想这也可能是为什么在ThreadLocal的实现中要将ThreadLocalMap放到Thread对象内部 ,它能随着Thread对象的释放而自动释放,省心。
四、总结
1、每个Thread对象都包含一个 ThreadLocalMap对象,用来存储要传递共享的数据
2、ThreadLocal并不存储数据,它只是提供了操作Thread对象中map对象的操作入口
3、ThreadLocal通过当前线程对象Thread.currentThread()来路由,可以轻松路由到正确的thread对象以及map对象上,解决线程安全问题
4、经常使用ThreadLocal.remove() 方法可以及时释放map中的资源,防止意外情况发生