javaapm

ThreadLocal及其扩展

2024-04-24  本文已影响0人  修行者12138

ThreadLocal使用

用于相同线程内上下文的传递,避免显式传参,简化代码。
比如controller层把用户信息set到ThreadLocal,在service层get获取,无需显式传参。

ThreadLocal原理

Thread类中有一个threadLocals变量,类型为ThreadLocalMap

public class Thread {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap是一个自定义的hash map,底层是Entry数组,提供了getEntry(ThreadLocal<?> key) 、set(ThreadLocal<?> key, Object value)等api,使用线性探测法处理hash冲突。
从api可以看出,ThreadLocalMap的key是ThreadLocal对象,value是泛型对象值。

ThreadLocal的值,存储在Thread对象实例的堆内存空间里,而不是ThreadLocal对象,ThreadLocal只是提供了get set方法用于维护变量值。

InheritableThreadLocal使用

Thread类中,除了有threadLocals变量,还有一个inheritableThreadLocals变量

public class Thread {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

假设这样一个场景: 父线程开了一个子线程,但是我们希望在子线程中可以访问父线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:

public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new ThreadLocal();

    for (int i = 0; i < 10; i++) {
        // 父线程set
        threadLocal.set(i);

        new Thread(() -> {
            // 子线程get
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }).start();
    }
}

执行结果

Thread-0: null
Thread-2: null
Thread-1: null
Thread-4: null
Thread-3: null
Thread-5: null
Thread-6: null
Thread-7: null
Thread-8: null
Thread-9: null

如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal

public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new InheritableThreadLocal();

    for (int i = 0; i < 10; i++) {
        // 父线程set
        threadLocal.set(i);

        new Thread(() -> {
            // 子线程get
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }).start();
    }
}

再次执行

Thread-0: 0
Thread-2: 2
Thread-1: 1
Thread-3: 3
Thread-4: 4
Thread-5: 5
Thread-6: 6
Thread-7: 7
Thread-8: 8
Thread-9: 9

可以看到,每个子线程都可以访问到从父线程传递过来的一个数据。

InheritableThreadLocal原理

InheritableThreadLocal源码如下,可以看到InheritableThreadLocal重写了父类的几个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

重写createMap方法是为了把变量存到Thread里的inheritableThreadLocals变量,而不是threadLocals变量。

Thread类的构造方法,会调用init方法。
调用子线程Thread类的init方法时,如果parent.inheritableThreadLocals不为空,会把parent.inheritableThreadLocals浅拷贝到子线程的inheritableThreadLocals.

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

实际业务里,InheritableThreadLocal的使用场景很少,主要原因如下

  1. 上下文的拷贝是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不行了,因为线程池的核心线程是提前创建的
  2. 父子线程的map里的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题(父子线程同时访问)

TransmittableThreadLocal原理

阿里TransmittableThreadLocal依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

如果使用线程池,有3个时机需要考虑: 创建子线程、提交任务、执行任务

  1. 线程池的核心线程在线程池初始化时就创建了,创建时机远远早于用户请求。
  2. 提交任务后,不一定会马上执行,有可能核心线程数用完了,需要在阻塞队列里等待。
  3. 最好是在执行任务前,把父线程的上下文拷贝到子线程。

假设一个请求打到tomcat,tomcat从线程池里取出线程A(父线程),在controller层设置了用户信息到线程A的ThreadLocal。然后进入service层,使用线程池,从线程池里拿出线程B执行业务逻辑。

线程A提交任务后,就可以处理别的请求了。

如果等到线程B执行任务前,才从线程A拷贝上下文,这时候线程A的ThreadLocal可能被其他请求的数据覆盖了。

因此,线程A提交任务时,就需要把上下文拷贝到一个地方,然后线程B执行任务前,再从那个地方拷贝上下文。

总结

ThreadLocal: 无法复制上下文至子线程中
InheritableThreadLocal: 创建子线程时,从父线程复制上下文
TransmittableThreadLocal: 子线程执行任务时,从父线程复制上下文

上一篇下一篇

猜你喜欢

热点阅读