ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal原理及Demo

1.ThreadLocal

[1.1 原理](#1.1 原理)

[1.2 Demo](#1.2 Demo)

[1.3 应用场景](#1.3 应用场景)

2.InheritableThreadLocal

[2.1 原理](#2.1 原理)

[2.2 Demo](#2.2 Demo)

[2.3 应用场景](#2.3 应用场景)

3.TransmittableThreadLocal

[3.1 原理](#3.1 原理)

[3.2 Demo](#3.2 Demo)

3.3应用场景

1.ThreadLocal

1.1 原理

造成ThreadLocal内存泄露的主要原因是:

key是弱引用,在下一次GC的时候被回收了,但是value(entry)没有被及时回收;此时value如果一直不能被访问到,就会造成value一直存活,所以导致内存泄漏

但是ThreadLocal本身将key设置为弱引用就是为了最大程度的减少内存泄漏的问题,当key被GC后,key变为null。ThreadLocal可以通过key.get()==null来判断Key是否已经被回收,如果Key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。回收的时机:

1)调用set()方法时,采样清理、全量清理,扩容时还会继续检查。

如果Entry.get()==null说明发生哈希冲突了,且旧Key已经被回收了,此时ThreadLocal会替换掉旧的value,避免发生「内存泄漏」。

如果没有哈希冲突,Thr eadLocal仍然会调用cleanSomeSlots来清理部分节点

2)调用get()方法,没有直接命中,向后环形查找时。

如果命中则直接返回,如果没有命中则可能是哈希冲突了、或者Key不存在/已被回收,接着调用getEntryAfterMiss()查找,这里也会进行过期节点的清理。

3)调用remove()时,除了清理当前Entry,还会向后继续清理。

线程调用ThreadLocal.remove()本身就是清理当前节点的,但是为了避免发生「内存泄漏」,ThreadLocal还会检查容器中是否还有其他过期节点,如果发现也会一并清理,主要逻辑在ThreadLocalMap.remove()

1.2 Demo

java 复制代码
package com.cocoa.threadLocalDemo;

/**
 * ThreadLocal解决的是让每个线程读取的ThreadLocal变量是相互独立的
 * 子线程 无法直接复用 父线程的ThreadLocal变量里的内容
 * 每个线程 都有一个 ThreadLocalMap
 */
public class TestThreadLocal implements Runnable {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        // 主线程设置值到ThreadLocal
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());

        Thread tt = new Thread(new TestThreadLocal());// 这里想要把主线程的ThreadLocal传给子线程
                                                     // 但是Thread源码中init并没有将主线程的信息设置到子线程中
        tt.start();
        System.out.println("----主线程结束");
    }
 
    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

1.3 应用场景

ThreadLocal 适用于如下两种场景

  • 1、每个线程需要有自己单独的实例
  • 2、实例需要在多个方法中共享,但不希望被多线程共享

场景1:存储用于session

场景2:数据库连接,处理数据库事务

场景3:数据跨层传递(controller,service, dao)

场景4:Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的"状态性对象"采用ThreadLocal进行封装,让它们也成为线程安全的"状态性对象",因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

2.InheritableThreadLocal

2.1 原理

InheritableThreadLocal仅仅是重写了3个方法!

在new Thread的时候,InheritableThreadLocal会将父线程的ThreadLocal设置到子线程中!

在使用线程池的时候

InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果 ,因为线程池中的线程是复用的并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。

2.2 Demo

不带线程池的,正常使用

java 复制代码
package com.cocoa.InheritableThreadLocalDemo;
 
public class TestInheritableThreadLocal implements Runnable {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
 
    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());

        Thread tt = new Thread(new TestInheritableThreadLocal());
        tt.start();
        System.out.println("----主线程结束");
 
    }
 
    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

使用线程池之后:

java 复制代码
package com.cocoa.InheritableThreadLocalDemo;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class TestInheritableThreadLocalAndExecutor implements Runnable {
    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
 
    public static void main(String[] args) throws Exception{
        System.out.println("----主线程启动");
        inheritableThreadLocal.set("主线程第一次赋值");
        System.out.println("----主线程设置后获取值:" + inheritableThreadLocal.get());

        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        System.out.println("主线程休眠2秒");
        Thread.sleep(2000);
        
        inheritableThreadLocal.set("主线程第二次赋值");
        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        executorService.shutdown();
    }
 
    @Override
    public void run() {
        System.out.println("----子线程获取值:" + inheritableThreadLocal.get());
    }
}

3.TransmittableThreadLocal

TransmittableThreadLocal是阿里开源的一个,主要目的是处理父子线程变量不能共用的情况。

3.1 原理

核心源码:在set方法中的addValue中,使用到Holder保存线程的信息,然后copy到其他线程中使用

TransmittableThreadLocal使用的2种方式

  • 使用TtlRunnable get(@Nullable Runnable runnable)装饰Runnable或使用TtlCallable get(@Nullable Callable callable)装饰callable
  • ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService)装饰executorService

3.2 Demo

直接使用ExecutorService的线程池:

java 复制代码
package com.cocoa.transmittableThreadLocalDemo;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * ttl 结合 线程池 使用
 */
public class TestTransmittableThreadLocal02 {
    public static void main(String[] args) {
        final TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
        transmittableThreadLocal.set("aaa");

        Runnable runnable = () -> {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("====" + Thread.currentThread().getId() + "====");
            System.out.println(transmittableThreadLocal.get());
        };

        ExecutorService executorService = Executors.newFixedThreadPool(1);

        int i = 0;
        while(true) {
            executorService.execute(TtlRunnable.get(runnable));// 开启线程池
            transmittableThreadLocal.set(i + "AA");
            i ++;
        }
    }
}

使用TtlExecutors装饰executorService:

java 复制代码
package com.cocoa.transmittableThreadLocalDemo;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestTransmittableThreadLocal {
    public static void main(String[] args)  {
        final TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
        transmittableThreadLocal.set("aaa");

        Runnable runnable = () -> {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("====" + Thread.currentThread().getId() + "====");
            System.out.println(transmittableThreadLocal.get());
        };

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);// TtlExecutors装饰executorService

        int i = 0;
        while(true) {
            ttlExecutorService.execute(runnable);
            transmittableThreadLocal.set(i + "AA");
            i ++;
        }
    }
}

3.3 应用场景

  1. Web 请求上下文传递: 在处理 Web 请求时,可能会在主线程中设置一些请求相关的信息 (例如用户信息、请求 ID 等),然后在异步任务中继续使用这些信息 。使用 TransmittableThreadLocal 可以确保在主线程和异步任务中能够正确地传递和使用这些请求上下文信息,而不会出现丢失或混乱。
  2. 日志追踪: 在日志记录中,可能需要在不同线程中记录同一个请求的日志信息 ,以便进行请求追踪和调试。使用 TransmittableThreadLocal 可以确保日志记录器能够在不同线程之间正确传递和使用请求相关的日志上下文信息,而不会丢失或混乱。
  3. 缓存处理: 在缓存处理中,可能需要在多个线程中共享同一个缓存对象 ,并且需要在不同线程中更新和读取缓存数据。使用 TransmittableThreadLocal 可以确保缓存对象在不同线程之间正确传递和共享数据,而不会出现数据不一致的情况。
  4. 任务分发和处理: 在任务分发和处理的场景中,可能需要在不同线程中共享任务相关的上下文信息 ,并且需要在不同线程中正确处理和传递任务。使用 TransmittableThreadLocal 可以确保任务相关的上下文信息能够在不同线程之间正确传递和使用,从而确保任务的正确处理和执行。
相关推荐
积水成江1 小时前
Vite+Vue3+SpringBoot项目如何打包部署
java·前端·vue.js·windows·spring boot·后端·nginx
2c237c62 小时前
[ 蓝桥 ·算法双周赛 ] 第 19 场 小白入门赛
算法·蓝桥云课·算法双周赛
猫武士水星2 小时前
微信步数C++
算法
这可就有点麻烦了2 小时前
强化学习笔记之【DDPG算法】
笔记·算法·机器学习
2401_857439693 小时前
SpringBoot在线教育平台:设计与实现的深度解析
java·spring boot·后端
总是学不会.3 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven
IT学长编程4 小时前
计算机毕业设计 视频点播系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·视频点播系统
极客小张4 小时前
基于STM32的智能家居语音控制系统:集成LD3320、ESP8266设计流程
c语言·stm32·物联网·算法·毕业设计·课程设计·语言识别
一 乐5 小时前
英语词汇小程序小程序|英语词汇小程序系统|基于java的四六级词汇小程序设计与实现(源码+数据库+文档)
java·数据库·小程序·源码·notepad++·英语词汇