首页 > 其他分享 >一文详解ThreadLocal与线程间的数据传递

一文详解ThreadLocal与线程间的数据传递

时间:2024-04-08 20:31:57浏览次数:13  
标签:threadLocal value ThreadLocal 详解 InheritableThreadLocal 线程 null

一. ThreadLocal

ThreadLocal在并发编程中比较常用,在诸多中间件的源码中都能看到它的身影。

对于ThreadLocal的说明先来一段官方解释:

ThreadLocal提供的是一种线程局部变量。这些变量不同于其它变量的点在于每个线程在获取变量的时候,都拥有它自己相对独立的变量副本。ThreadLocal 的实例一般是私有静态的,可以做到与一个线程绑定某一种状态。

根据官方的阐述可知ThreadLocal设计思路:线程隔离 。将变量缓存在当前线程的内部,只对本线程可见(与线程对象Thread绑定)对其他线程是隔离的。

总结起来一句话:没有共享 就没有伤害

1. 实现原理

在探讨一项技术或者学习一项新技术时最简单的方式就是从一个简单的使用示例入手

public class ThreadLocalDemo {

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        //添加数据
        threadLocal.set("内部变量");
        //do something
        System.out.println(threadLocal.get());
    }
}

上面是ThreadLocal所使用的简单示例,对于不了解ThreadLocal的同学可能会有疑问。前文中说到ThreadLocal是和线程绑定的,但是在使用示例中并没有关于ThreadLocal和Thread的关系。

答案在于ThreadLocal的内部对象ThreadLocalMap。

Threrad类声明的ThreadLocalMap变量:

    /* 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.set源码:

    /**
     * 为当前 ThreadLocal 对象关联 value 值
     * @param value 要存储在此线程的线程副本的值
     */
    public void set(T value) {
        // 返回当前 ThreadLocal 所在的线程
        Thread t = Thread.currentThread();
        // 返回当前线程持有的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
            map.set(this, value);
        } else {
            // 否则,创建ThreadLocalMap并存储value
            createMap(t, value);
        }
    }

 ThreadLocal的set/get/remove方法都是对ThreadLocalMap的处理

ThreadLocalMap内部结构

下面我们继续探究ThreadLocalMap还是从源码开始研究:

static class ThreadLocalMap {

        /**
         * 键值对实体的存储结构
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /**
             * 当前线程关联的 value,这个 value 并没有用弱引用追踪
             */
            Object value;

            /**
             * 构造键值对
             *
             * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
             * @param v v 作 value
             */
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        。。。
}

在代码中看到了Map、看到了Entry,很容易让我们想到熟悉的HashMap。其实ThreadLocalMap它也是一个KV结构的,不同于HashMap它使用的解决哈希冲突的方式是开放寻址法。另外它的Key继承了WeakReference代表是一个弱引用。

本文对于ThreadLocalMap的解决哈希冲突和扩容逻辑等不展开讨论,只介绍实践应用中的常见问题。后续如果有时间可以单独开设源码文章。

看到这里先捋清各自的关系,看下对象间的引用链路:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

面试题:ThreadLocal为什么会出现内存泄漏?

想必这个面试题在大家提到ThreadLocal时很常见,搞清楚这个问题需要先讲清楚弱引用的作用。

弱引用是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

在实际项目中我们都会使用线程池来管理线程,对于核心线程一般情况下都是循环利用。也就是说核心线程很大程度上是伴随着整个项目生命周期的。JUC的作者为了照顾我们这些糊涂程序员,为了避免无限制的向ThreadLocalMap内填充数据而不进行删除所导致的内存泄漏问题。对ThreadLocal多了一些特殊的设计。

由上图可以看到ThreadLocal对象被ThreadLocal变量和Entry的key所引用。要想对ThreadLocal对象进行垃圾回收需要断开ThreadLocal变量和Entry的key对其的引用。首先在ThreadLocal变量使用结束后就会失去对ThreadLocal对象的引用

而将Entry的key设置成弱引用。为了在发生GC后Entry的key就会失去对ThreadLocal对象的引用。最终ThreadLocal对象没有了其他引用在下一次GC时就可以被回收掉了。

但是如果Entry的key不是弱引用而是强引用,即便ThreadLocal变量使用结束失去对ThreadLocal对象的引用。ThreadLocal对象也无法回收掉因为它还会被Entry的Key所引用。这就是Entry的key设置成弱引用的原因。

解决了Key的问题,还需要进一步处理value。Entry的key置为空后便无法对Entry的value进行取值了,如果value不能被清理还是会出现内存泄漏。对于这种场景Doug Lea的设计思路是在其他ThreadLocal使用set/get/remove方法对ThreadLocalMap进行操作时会清理掉ThreadLocalMap中Entry的key为null的值

下面以ThreadLocalMap的getEntry方法为例:

        /**
         * 返回 key 关联的键值对实体
         *
         * @param key threadLocal
         * @return
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回
            if (e != null && e.get() == key) {
                return e;
            } else {
                // 从 i 开始向后遍历找到键值对实体
                return getEntryAfterMiss(key, i, e);
            }
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    return e;
                }
                // 遇到key为null
                if (k == null) {
                    // 从索引 i 开始清理key为null的值
                    // 将Entry的value设置为null,防止内存泄漏
                    expungeStaleEntry(i);
                } else {
                    i = nextIndex(i, len);
                }
                e = tab[i];
            }
            return null;
        }

至此即便使用了线程池中的循环应用线程池、即便我们忘记了调用ThreadLocal的remove方法。JUC的设计者帮我们减少了内存泄漏问题的发生。

    /**
     * 使用1个线程的线程池
     * 确保两次调用都是同一线程处理
     */
    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    @GetMapping("/setOne")
    public void handleOne() {
        executorService.execute(() -> {
            VersionThreadLocal threadLocalOne = new VersionThreadLocal();
            threadLocalOne.set(new VersionThreadLocalValue());
            //do something
            //清除VersionThreadLocal
            System.gc();
        });
    }

    @GetMapping("/setTwo")
    public void handleTwo() {
        executorService.execute(() -> {
            VersionThreadLocal threadLocalTwo = new VersionThreadLocal();
            threadLocalTwo.get();
            //do something
            //清除Entry的value 也就是VersionThreadLocalValue
            System.gc();
        });
    }


//新建自定义ThreadLocal,主要是方便堆栈工具中查看类
public class VersionThreadLocal extends ThreadLocal<VersionThreadLocalValue>{
}

调用第一个接口可以看到VersionThreadLocal已经被垃圾回收,但是VersionThreadLocalValue对象还存在。

调用第二个接口因为执行了其他ThreadLocal对 ThreadLocalMap的操作,所以把Entry的Key为null的Value值也设置为null。GC后也就是对VersionThreadLocalValue进行了垃圾回收。

虽然ThreadLocal设置的已经很巧妙了,但是如果我们没有进行第二步的操作。那么VersionThreadLocalValue对象会一直存在内存中,造成了内存泄漏。虽然这种情况的内存泄漏对我们的系统来讲不会引发严重问题,但是我们还是需要去避免。

规范做法:

    public void handleOne() {
        executorService.execute(() -> {
            VersionThreadLocal threadLocalOne = new VersionThreadLocal();
            try{
                threadLocalOne.set(new VersionThreadLocalValue());
                //do something
                //清除VersionThreadLocal
                System.gc();
            }catch (Exception e){
                
            }finally {
                //主动执行remove方法
                threadLocalOne.remove();
            }
        });
    }

2. ThreadLocal在源码中的应用

XXL-Job分布式定时任务中间件使用ThreadLocal设置统一日期转换格式

/**
 * date util
 *
 * @author xuxueli 2018-08-19 01:24:11
 */
public class DateUtil {

    ...

    private static final ThreadLocal<Map<String, DateFormat>> dateFormatThreadLocal = new ThreadLocal<Map<String, DateFormat>>();
    private static DateFormat getDateFormat(String pattern) {
        if (pattern==null || pattern.trim().length()==0) {
            throw new IllegalArgumentException("pattern cannot be empty.");
        }

        Map<String, DateFormat> dateFormatMap = dateFormatThreadLocal.get();
        if(dateFormatMap!=null && dateFormatMap.containsKey(pattern)){
            return dateFormatMap.get(pattern);
        }

        synchronized (dateFormatThreadLocal) {
            if (dateFormatMap == null) {
                dateFormatMap = new HashMap<String, DateFormat>();
            }
            dateFormatMap.put(pattern, new SimpleDateFormat(pattern));
            dateFormatThreadLocal.set(dateFormatMap);
        }

        return dateFormatMap.get(pattern);
    }
    ...
}

 Spring框架对于ThreadLocal的使用示例

/**
 * {@link ThreadLocal} subclass that exposes a specified name
 * as {@link #toString()} result (allowing for introspection).
 *
 * @author Juergen Hoeller
 * @since 2.5.2
 * @param <T> the value type
 * @see NamedInheritableThreadLocal
 */
public class NamedThreadLocal<T> extends ThreadLocal<T> {

	private final String name;


	/**
	 * Create a new NamedThreadLocal with the given name.
	 * @param name a descriptive name for this ThreadLocal
	 */
	public NamedThreadLocal(String name) {
		Assert.hasText(name, "Name must not be empty");
		this.name = name;
	}

	@Override
	public String toString() {
		return this.name;
	}

}

二. InheritableThreadLocal

ThreadLcoal只可以使用在本线程内,不能处理父线程向子线程传递数据的场景。

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("传递变量");
        Thread thread = new Thread(() -> {
            System.out.println("获取变量 :" + threadLocal.get());
        });
        thread.start();
    }

    ---
    获取变量 :null

需要解决父线程向子线程传递数据的问题,需要使用InheritableThreadLocal

    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("传递变量");
        Thread thread = new Thread(() -> {
            System.out.println("获取变量 :" + threadLocal.get());
        });
        thread.start();
    }

    ---
    获取变量 :传递变量

 1. 实现原理

InheritableThreadLocal同样是在Thread类里面声明的和ThreadLocal大致相同 

    /* 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
 * @see ThreadLocal
 */
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 拿到父线程的值后,可以在这里处理后再返回给子线程
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 获取当前线程内的 inheritableThreadLocals 属性
     *
     * @param t 当前线程
     */
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }

    ...
}

查看源码可知InheritableThreadLocal继承了ThreadLocal方法,但是对ThreadLcoal的一些核心方法并没有重写。那它又是如何实现父线程向子线程做数据传递呢,答案在Thread中。在每次新创建线程Thread时会执行私有方法init。

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        // 当前线程作为父线程
        Thread parent = currentThread();
        

        /* 忽略无关代码 */
        
        
        // 当父线程的 inheritableThreadLocals 的值不为空时
        // 会把 inheritableThreadLocals 里面的值全部传递给子线程
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        // 线程 id 自增
        tid = nextThreadID();
    }

在每次新建线程时父线程的InheritableThreadLocal会传递给子线程的InheritableThreadLocal。也就是把父线程的ThreadLocalMap传递给子线程。

2. 总结

由此可总结实现父线程向子线程传递需要两个步骤

  • 父线程的InheritableThreadLocal不为空
  • 子线程是新建线程

三. TransmittableThreadLocal

在实际项目中很少会频繁创建线程对象,一般会使用线程池。很大程度上线程池内的线程都是复用的不会频繁新建。这种场景对于InheritableThreadLocal是不满足使用条件的。

InheritableThreadLocal生效场景
    /**
     * 使用1个线程的线程池
     * 确保两次调用都是同一线程处理
     */
    private static ExecutorService executorService = Executors.newSingleThreadExecutor();
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        threadLocal.set("传递变量");
        executorService.execute(()->{
            System.out.println("executorService创建线程");
        });
        //线程休眠
        Thread.sleep(100);
        executorService.execute(()->{
            System.out.println("获取变量 :"+threadLocal.get());;
        });
    }

    ---
    executorService创建线程
    获取变量 :传递变量
InheritableThreadLocal不生效场景
    /**
     * 使用1个线程的线程池
     * 确保两次调用都是同一线程处理
     */
    private static ExecutorService executorService = Executors.newSingleThreadExecutor();
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        executorService.execute(()->{
            System.out.println("executorService创建线程");
        });
        //线程休眠
        Thread.sleep(100);
        threadLocal.set("传递变量");
        executorService.execute(()->{
            System.out.println("获取变量 :"+threadLocal.get());;
        });
    }

    ---
    executorService创建线程
    获取变量 :null

两个场景的区别在于threadLocal.set("传递变量");的位置不同。生效场景InheritableThreadLocal的set方法在线程池创建线程之前,而不生效场景InheritableThreadLocal的set方法在创建线程之后(线程池内已经存在了可供使用的核心线程)。

对于父线程向已存在的线程传递数据需要用到阿里的TransmittableThreadLocal。

 官网:https://github.com/alibaba/transmittable-thread-local

<!-- 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>
    /**
     * 使用1个线程的线程池
     * 确保两次调用都是同一线程处理
     */
    private static final Executor ttl_executor = TtlExecutors.getTtlExecutor(Executors.newSingleThreadExecutor());
    private static TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ttl_executor.execute(()->{
            System.out.println("executorService创建线程");
        });
        //线程休眠
        Thread.sleep(100);
        threadLocal.set("传递变量");
        ttl_executor.execute(()->{
            System.out.println("获取变量 :"+threadLocal.get());;
        });
    }


    ---
    executorService创建线程
    获取变量 :传递变量

1. TransmittableThreadLocal.set

对于TransmittableThreadLocal的介绍还是从简单使用入手,TransmittableThreadLocal的set方法做了哪些逻辑。

    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && value == null) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }

没有太复杂的逻辑,除了调用父类的set方法。主要在addThisToHolder

    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }



    // Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        the value of WeakHashMap is *always* null, and never used.
    //    2.2 WeakHashMap support *null* value.
    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    return new WeakHashMap<>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    return new WeakHashMap<>(parentValue);
                }
            };

第一个重要变量holder,根据官方的代码注释可以把它理解为一个set集合。里面的值是各个TransmittableThreadLocal对象,也就是说项目内通过TransmittableThreadLocal.set方法设置了值的TransmittableThreadLocal对象都在这个holder中。

2. TtlRunnable

根据官网的介绍TransmittableThreadLocal的实现原理。是使用TtlRunnableTtlCallable来修饰传入线程池的RunnableCallable。上面示例使用的TtlExecutors.getTtlExecutor最终也是执行RunnableCallable

 下面是官网提供的调用时序图。

public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
    private final AtomicReference<Capture> capturedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //capture用于捕获父线程的ttl,会将当父线程上下文保存起来;
        this.capturedRef = new AtomicReference<>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        //取出ttl
        final Capture captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        //给当前子线程将父线程的TTL循环复制进子线程,
        //返回的backup是此子线程原来就有的本地变量值(子线程的TTLMap);
        //backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
        final Backup backup = replay(captured);
        try {
            runnable.run();
        } finally {
            //方法用来恢复原有值的
            restore(backup);
        }
    }
    ...
}

看到这里其实就可以捋清TransmittableThreadLocal的大致流程了。使用自定义的类TtlRunnable对Runnable进行扩展。线程池新建执行任务时(TtlRunnable)获取到项目中所有的ttl(使用holder做记录,还需要执行转换逻辑),执行run方法前取出TransmittableThreadLocal的快照(capture方法)并复制给将要执行任务的线程并返回backup(子线程原本的变量),在run方法执行结束后再将backup恢复给子线程。

3. Transmittee

经过上面的分析对于Transmittee的逻辑就方便理解了。主要就是TTL快照数据的获取与转换,根据代码和注释理解起来不成问题,就不展开讨论了。

    private static class TtlTransmittee implements Transmittee<HashMap<TransmittableThreadLocal<Object>, Object>, HashMap<TransmittableThreadLocal<Object>, Object>> {
        @NonNull
        @Override
        public HashMap<TransmittableThreadLocal<Object>, Object> capture() {
            final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = newHashMap(holder.get().size());
            for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
                ttl2Value.put(threadLocal, threadLocal.getTransmitteeValue());
            }
            return ttl2Value;
        }

        @NonNull
        @Override
        public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
            final HashMap<TransmittableThreadLocal<Object>, Object> backup = newHashMap(holder.get().size());

            for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
                TransmittableThreadLocal<Object> threadLocal = iterator.next();

                // backup
                backup.put(threadLocal, threadLocal.get());

                // clear the TTL values that is not in captured
                // avoid the extra TTL values after replay when run task
                if (!captured.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            // set TTL values to captured
            setTtlValuesTo(captured);

            return backup;
        }

        @NonNull
        @Override
        public HashMap<TransmittableThreadLocal<Object>, Object> clear() {
            return replay(newHashMap(0));
        }

        @Override
        public void restore(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
            for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
                TransmittableThreadLocal<Object> threadLocal = iterator.next();

                // clear the TTL values that is not in backup
                // avoid the extra TTL values after restore
                if (!backup.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            // restore TTL values
            setTtlValuesTo(backup);
        }
}

对于线程任务的封装除了修饰Runnable和Callable和修饰线程池(推荐)外还提供了使用Java Agent来修饰JDK线程池实现类(和SkyWalking的思路一致

3. TTL使用场景

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. SessionCache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

四. MDC线程间的数据传递

MDC(Mapped Diagnostic Context,映射调试上下文)是 slf4j 提供的一种轻量级的日志跟踪工具。
Log4j、Logback或者Log4j2等日志中最常见区分同一个请求的方式是通过线程名,而如果请求量大,线程名在相近的时间内会有很多重复的而无法分辨,因此引出了trace-id,即在接收到的时候生成唯一的请求id,在整个执行链路中带上此唯一id。
MDC.java本身不提供传递traceId的能力,真正提供能力的是MDCAdapter接口的实现。比如Log4j的是Log4jMDCAdapter,Logback的是LogbackMDCAdapter。

MDC使用的是ThreadLocal和InheritableThreadLocal,可以做到本线程间或者新建子线程的数据传递。对于实际使用中涉及线程池的数据传递需要一些改造。

1. MDC的包装类

新建MDCRunnable对Runnable进行封装

public class MDCRunnable implements Runnable{

    private Map<String, String> mdcContext;
    private Runnable runnable;

    public MDCRunnable(Runnable runnable) {
        this.runnable = runnable;
        this.mdcContext = MDC.getCopyOfContextMap();
    }

    @Override
    public void run() {
        if (mdcContext != null) {
            MDC.setContextMap(mdcContext);
        }
        try {
            runnable.run();
        } finally {
            MDC.clear();
        }
    }
}

在线程池执行任务时使用MDCRunnable来包装任务

     /**
     * 使用1个线程的线程池
     * 确保两次调用都是同一线程处理
     */
    private static ExecutorService executorService = Executors.newSingleThreadExecutor();

    public static void main(String[] args) throws InterruptedException {
        executorService.execute(new MDCRunnable(()->{
            System.out.println("executorService创建线程");
        }));
        //线程休眠
        Thread.sleep(100);
        MDC.put("traceId","9527");
        executorService.execute(new MDCRunnable(()->{
            System.out.println("获取变量 :"+MDC.get("traceId"));
        }));
    }

    ---
    executorService创建线程
    获取变量 :9527

2. MDCAdapter的扩展

这部分的扩展是来自TransmittableThreadLocal对MDC的支持,目前对于Log4J2、Logback都实现了扩展方式。

Log4J2方式:

transmittable-thread-local/docs/requirement-scenario.md at master · alibaba/transmittable-thread-local · GitHubLogback方式:

transmittable-thread-local/docs/requirement-scenario.md at master · alibaba/transmittable-thread-local · GitHub

3. 总结

从方法上来看实现对线程池内循环使用线程的数据传递。基本思路都是在InheritableThreadLocal的基础上进行扩展。在执行Runnable或Callable任务时对其包装,在真正调用线程run方法前后设置数据快照的复制替换和传递。比较简单的做法是实现包装类(比如上面提到的MDCRunnable)进行处理。但是这种方式是对代码入侵的,对开发来讲不太友好。比较好的方式是使用java agent来扩展,对字节码进行增强。例如TransmittableThreadLocal和SkyWalking都使用了这种方式,无需改动代码即可实现。

标签:threadLocal,value,ThreadLocal,详解,InheritableThreadLocal,线程,null
From: https://blog.csdn.net/weixin_43966424/article/details/137470049

相关文章

  • 【WPF应用41】WPF中的Expander控件详解
    WindowsPresentationFoundation(WPF)中的Expander控件是一个用于显示详细信息的交互式UI元素。它允许用户通过点击标题来展开或折叠内容区域。Expander控件通常用于在界面上组织内容,提供一种可见/隐藏的功能,以帮助用户专注于当前感兴趣的信息。一、Expander控件简介Expand......
  • 线程礼让
    线程礼让,让当前正在执行的线程暂停,但不阻塞将线程从运行状态转为就绪状态让CPU重新调度,礼让不一定成功,只要还是看CPU的调度。packageStateThread;publicclassTestYield{publicstaticvoidmain(String[]args){MyYields1=newMyYield();new......
  • C语言:指针详解(1)
    目录一、内存和地址二、指针变量和地址三、指针变量类型的意义四、const修饰指针五、指针运算六、野指针七、assert断言八、指针的使用和传值调用在正式学习指针之前,我们先要理解在C/C++中两个非常重要的概念——内存和地址。正是有这两种因素的存在,才使得C/C++拥有......
  • Linux curl命令详解
    Linuxcurl命令详解发布时间:2014-10-2710:25:36来源:linux网站作者:linux人命令:curl在Linux中curl是一个利用URL规则在命令行下工作的文件传输工具,可以说是一款很强大的http命令行工具。它支持文件的上传和下载,是综合传输工具,但按传统,习惯称url为下载工具。语法:#curl[op......
  • 从输入URL到页面渲染的全过程详解
    当我们在浏览器中输入一个URL并按下回车键时,背后其实发生了一系列的复杂过程。这个过程涉及到了网络协议、服务器处理、数据传输等多个环节。下面,我们将详细解析这一过程。一、URL解析当我们在浏览器中输入URL并回车后,浏览器首先会进行URL解析。URL(UniformResourceLocat......
  • Linux基本命令入门详解
    Linux基本命令是Linux系统操作的基础,掌握这些命令对于初学者来说至关重要。下面将详细介绍一些常用的Linux基本命令,并附上实际例子。一、目录操作命令pwd:显示当前所在的目录路径。例子:在终端中输入pwd,将显示当前用户所在的目录路径,如/home/user。cd:切换目录。例子:输......
  • JUC:ThreadPoolExecutor线程池的使用方法
    文章目录ThreadPoolExecutor线程池状态构造方法Executors工厂方法newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor提交任务方法关闭任务方法ThreadPoolExecutor线程池状态线程池用高三位表示状态,第一位为符号位。TERMINATED>TIDYING>STOP>......
  • 线程休眠
    Sleep指定当前线程阻塞的毫秒数;Sleep存在异常InterruptedException;Sleep时间达到后线程进入就绪状态;每个对象都有一把锁,Sleep不会释放锁;以下代码为利用Sleep进行模拟倒计时packageStateThread;//模拟倒计时publicclassTestSleep2{publicstaticvoidmain(Strin......
  • 自动编号工具类:NumAutoUtils详解
    在软件开发中,经常需要生成唯一的编号,例如订单号、发票号、实验编号等。为了简化这一过程,本文将介绍一个Java工具类NumAutoUtils,它可以帮助我们生成带有前缀和日期的自动编号。概述NumAutoUtils是一个Java工具类,它提供了两种方法来生成编号:getArMaxNum和getArMaxNum2。这些方法能......
  • JavaEE初阶Day 5:多线程(3)
    目录Day5:多线程(3)1.join2.再谈sleep3.线程的状态4.线程安全问题Day5:多线程(3)多线程在整个编程中都是非常核心非常重要的话题多核CPU客观的主流的需求多线程这里还是有一定难度/不少注意事项的回顾Thread创建的写法继承Thread,重写run实现Runnable,重写run......