JDK源码系列:ThreadLocal实现原理

 

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

一、ThreadLocal的本质

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

二、图解ThreadLocal的实现原理

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

1、Thread

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

2、ThreadLocal

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

3、ThreadLocalMap

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

4、Entry

数组元素。

JDK源码系列:ThreadLocal实现原理_第1张图片

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

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

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

JDK源码系列:ThreadLocal实现原理_第2张图片

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

JDK源码系列:ThreadLocal实现原理_第3张图片

JDK源码系列:ThreadLocal实现原理_第4张图片

JDK源码系列:ThreadLocal实现原理_第5张图片

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永远也用不了了,这就相当于内存泄漏了。

JDK源码系列:ThreadLocal实现原理_第6张图片

5、如何避免泄漏?

1)在一个线程每一次运行生命周期的起始端调用 threadLocal.set(o)方法来重置覆盖上次可能遗留的资源

2)在一个线程每一次运行生命周期的末端 调用  threadLocal.remove()方法是否绑定的资源

3)threadLocal一般声明为static类型的对象,避免频繁创建与释放

三、既然明白了ThreadLocal的原理,有没有考虑过自己手写一个工具实现类似功能?

//手撕一个线程绑定传递工具
public class MyMap {
     private Map> 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 map = threadMap.get(Thread.currentThread());
        if (map==null) {
            return null;
        }
        return map.get(key);
     }
}




//简单测试一把
public class ThreadLocalClient2 {
    static MyMap map = new MyMap();
    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中的资源,防止意外情况发生

 

你可能感兴趣的:(公众号:,老吕架构,JDK,java)