vlambda博客
学习文章列表

JDK源码系列:ThreadLocal实现原理

        

        大家好,在软件开发过程中,一般情况下方法之间调用时都是通过接口参数来传递数据的,但有一些公共参数(userId、token、orgId、roleId等)的传递就不能那么干了,在Java中一般用ThreadLocal 去解决这个问题,今天老吕来分析下ThreadLocal的源码。


一、ThreadLocal的本质


通过共享内存来传递数据。但它是如何做到多线程安全的呢?为什么数据就不会串呢?


二、图解ThreadLocal的实现原理


整个过程中参与方主要有这几个角色:


1、Thread

线程,每一个线程都会有一个私有的线程栈空间。


2、ThreadLocal

透明的做到了和线程绑定传递数据,实际上是一个操作 ThreadLocalMap的入口工具。


3、ThreadLocalMap

数组实现的Map。用来存储要传递的数据。


4、Entry

数组元素。


JDK源码系列:ThreadLocal实现原理


通过阅读源码,我画出了上面的图,可以看出:


1、要传递的数据实际上并没有在ThreadLocal中存储,而是在ThreadLocalMap中,ThreadLocalMap实际上位于Thread对象中。

上面的注释也说了,这个map被ThreadLocal class 维护。


JDK源码系列:ThreadLocal实现原理


2、ThreadLocal实际上只是一个操作 ThreadLocalMap的入口工具。


JDK源码系列:ThreadLocal实现原理

JDK源码系列:ThreadLocal实现原理

JDK源码系列:ThreadLocal实现原理


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-1zhangsan0pool-1-thread-2:pool-1-thread-2lishi0pool-1-thread-1:pool-1-thread-1zhangsan1pool-1-thread-2:pool-1-thread-2lishi1pool-1-thread-2:pool-1-thread-2lishi2pool-1-thread-1:pool-1-thread-1zhangsan2pool-1-thread-1:pool-1-thread-1zhangsan3pool-1-thread-2:pool-1-thread-2lishi3pool-1-thread-1:pool-1-thread-1zhangsan4pool-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中的资源,防止意外情况发生