一文吃透ThreadLocal的前世与今生

2,005 阅读9分钟

一.ThreadLocal介绍

ThreadLocal在日常工作中的使用频率还是很高的,但是大多使用场景比较单一,而且使用不当的情况下,会出现内存泄露的情况。

那么ThreadLocal到底是什么,我们先来看一下官方类注释。

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 * 
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 */  	

ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()方法操作此变量,并且不会与其他线程的局部变量进行冲突,保证了线程间局部变量的隔离行。

大白话:ThrealLocal类型的变量属于当前线程,哪怕是同一个方法内的同一个ThreadLocal变量,他们的值在不同的业务运行下也是不一样的,线程安全。

二.ThreadLocal业务应用

2.1.维护数据库连接

jdbc时代,数据库连接connection需要我们手动来维护,同一个库连接我们可以用数据库连接池来实现。但是不同数据库连接的时候,不同的线程需要获取到不同的连接。这时候就可以使用ThreadLocal来维护线程池中不同数据源连接。

demo我太懒就不贴了

贴一个网上人家实现的mybatis动态多数据源的实现

blog.csdn.net/u013360850/…

2.2.全局变量传递

业务开发中,对于当前请求用户数据的获取是一个很常见的需求。比如下订单的场景,我通过请求的token解析获取到用户数据,那用户数据可能在常见的MVC三层中都会被使用到。比较麻烦的方法是,在每个方法的入参都维护一个User参数进行传递。这样做的劣势在于,当前业务场景所关联的所有方法都多了一个User的入参。

image-20210426143554691.png

因为每个线程对应的用户信息可能都是不一样的,但是针对于一个token而言,用户信息的维护都是单一的。那么可以通过拦截器/过滤器/AOP在controller层解析token获取到User信息,放入ThreadLocal变量中,让当前线程相关的所有方法共享此变量,减少了,方法参数的传递。

image-20210426143920954.png

2.3.链路追踪

微服务架构下,多个服务之间调用如果出现了报错,使用链路追踪是一个常见的手段。在请求的线程变量中嵌入traceId,根据这个traceId就可以找到对应请求分别在各个业务应用的日志。

关于链路追踪up主写过一篇入门级的spring-cloud-sleuth文章,感兴趣的可以看看

juejin.cn/post/692300…

三.ThreadLocal原理解析

为了更好的理解ThreadLocal,阅读源码肯定的逃不开的了。面试的时候造个火箭也挺好不是吗? 重点解析一下set(),get(),remove()方法

3.1.set()方法

public void set(T value) {
		//获取当前线程
    Thread t = Thread.currentThread();
    //获取维护当前线程变量的ThreadLocalMap数据,一种类似于HashMap的数据结构
    ThreadLocalMap map = getMap(t);
    //如果当前线程已经存在了Map,直接调用map.set
    if (map != null)
        map.set(this, value);
    //不存在Map,则先进行新增map,再进行set
    else
        createMap(t, value);
}

set方法中出现了一个ThreadLocalMap这个数据结构,点进去看一下

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
		
		//代码太多不一一贴出来了
}

其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMap的Key是软引用维护的。这个地方很重要,至于为什么重要,后面再细说。

再继续点击一下发现ThreadLocal成员变量里面定义了这么一句话

ThreadLocal.ThreadLocalMap threadLocals = null;

这句话的出现表明了,针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。

3.2.get()方法

public T get() {
    Thread t = Thread.currentThread();
    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;
}
protected T initialValue() {
    return null;
}

get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。

3.3.remove()方法

//ThreadLocal
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
//ThreadLocalMap
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点

3.4.总结

1.Thread维护着一个ThreadLocalMap的引用,其中ThreadLocalMap的key为WeakReference维护

2.ThreadLocal本身并不存储值,ThreadLocal通过操作ThreadLocalMap达到对线程变量的赋值,获取,删除操作。

4.ThreadLocal的内存泄露问题

image-20210426152220803.png

看一下JVM中对ThreadLocal的堆栈维护图

entry对于value的引用为强应用,key的引用为弱引用

弱引用,强应用概念:blog.csdn.net/zalu9810/ar…

如果一个对象只是被弱引用引用者,那么只要发生 GC,不管内存空间是否足够,都会回收该对象。

那么问题就来了,如果操作ThreadLocal变量的方法QPS很高,疯狂被请求,这个时候你调用了set(),get()方法,并未调用remove方法,那么,当GC发生。entry与ThreadLocal的关联关系中断,Key被回收,value还被强连接关联着。这样跟垃圾回收可达性分析,value仍旧为可达,但是从业务角度上看,这个value值将永远访问不到,出现了内存泄露。

因此在使用ThreadLocal时必须要显示的调用remove方法,否则出现了问题,排查起来都很麻烦。

5.InheritableThreadLocal

日常工作中不可能所有工作都是基于单线程操作的。那在多线程情况下,主线程中定义的ThreadLocal变量,能在子线程中访问到吗?试一下看看

public class Demo {

    public static final ThreadLocal t = new ThreadLocal();

    public static void main(String[] args) {
        t.set("test");
        new Thread(() -> {
            System.out.println("new:"+t.get());
        }).start();
        System.out.println("main:"+t.get());
    }

}

输出:
main:test
new:null

淦,居然不能,这不凉了。

不要慌,开发java的大神早就考虑到我们的日常业务场景,在父子线程里面传递变量使用了InheritableThreadLocal,ThreadLocal是他的父类。试一下上面的代码

public class Demo {

    public static final ThreadLocal t = new InheritableThreadLocal();

    public static void main(String[] args) {
        t.set("test");
        new Thread(() -> {
            System.out.println("new:"+t.get());
        }).start();
        System.out.println("main:"+t.get());
    }

}

输出:
main:test
new:test

Nice~

我们来看看InheritableThreadLocal到底是何方神圣。

点击去这个类,比较简短,重写了三个ThreadLocal的方法

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
   	//查看1方法引用,逐层点击,可以发现这个初始化变量到了Thread这个类的init()方法中
    protected T childValue(T parentValue) {
        return parentValue;
    }

		//获取map被替换成了inheritableThreadLocals,而非threadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
   
   	//创建map的时候赋值使用inheritableThreadLocals,而非threadLocals
    void createMap(Thread t, T firstValue) t
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

第二个与第三个方法中比较简单,主要为原先使用ThreadLocal维护变量的地方都变成了inheritableThreadLocals。重点讲一下第一个方法,追溯到Thread类中init()方法,其中初始化代码中有这么一句话

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

将父线程的的线程Map赋值给了子线程,达到了,父子线程间的通信。

nice~

6.TransmittableThreadLocal

日常工作多线程编程使用直接新建线程的方式毕竟还是少数,更加规范的方式还是使用线程池。那么主线程中如果使用InheritableThreadLocal,主线程中能够传递线程变量进去吗。我们来试试看

public class Demo {

    public static final ThreadLocal t = new InheritableThreadLocal();

    public static void main(String[] args) {
        t.set("test");
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            System.out.println(t.get());
        });
        System.out.println("main:"+t.get());
    }

}


输出
main:test
test

成功了,nice~

测一下并发

public class Demo {

    public static final InheritableThreadLocal t = new InheritableThreadLocal();

    public static void main(String[] args) {
        t.set("test");
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Runnable runnable1 = () -> {
            System.out.println("new修改前:" + t.get());
        };
        Runnable runnable2 = ()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("new修改后:"+t.get());
        };
        executorService.submit(runnable1);
        System.out.println("main修改前:"+t.get());
        t.set("test1");
        System.out.println("main修改后:"+t.get());
        executorService.submit(runnable2);
        System.out.println("main最后:"+t.get());
    }

}


输出:
main修改前:test
new修改前:test
main修改后:test1
main最后:test1
new修改后:test

这下又懵了,修改后的InheritableThreadLocal变量未被修改,还是原来的值。

我们来回顾一下线程池与新建线程的区别,线程池的核心线程池在为开启释放核心线程池的情况下,将会被重复使用,但是InheritableThreadLocal中变量的维护在新建线城时才会进行一次赋值,所以出现了修改之后,新提交的任务,无法获取更新的InheritableThreadLocal变量值。

那这个问题怎么解决呢? TransmittableThreadLocal来帮你解决。这个是阿里开发的一种三方库,专门用来解决线程池内线程变量通信的问题,这里具体的原理不做展开解析了,贴上官网介绍

github.com/alibaba/tra…

我们来使用一下

public class Demo {

    public static final ThreadLocal t = new TransmittableThreadLocal();

    public static void main(String[] args) {
        t.set("test");
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Runnable runnable1 = () -> {
            System.out.println("new修改前:" + t.get());
        };
        Runnable runnable2 = ()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("new修改后:"+t.get());
        };
        executorService.submit(TtlRunnable.get(runnable1));
        System.out.println("main修改前:"+t.get());
        t.set("test1");
        System.out.println("main修改后:"+t.get());
        executorService.submit(TtlRunnable.get(runnable2));
        System.out.println("main最后:"+t.get());
    }

}

输出:
main修改前:test
new修改前:test
main修改后:test1
main最后:test1
new修改后:test1

nice~

7.总结

本文重点为大家介绍了ThreadLocal在各个日常业务开发场景下的应用,同时拓展介绍了InheritableThreadLocal在父子线程间通信的原理与方式,最后引入了阿里的TransmittableThreadLocal来支持主线程与线程池之间的线程变量通信,希望能让大家对ThreadLocal有一个系统的认知与帮助。

8.参考

juejin.cn/post/684490…

juejin.cn/post/684490…

9.联系我

钉钉:louyanfeng25

微信:baiyan_lou