ThreadLocal的使用及扩展

ThreadLocal

线程私有,无法跨线程传递

示例:在线程0中为threadLocal赋值,看另一个线程1和主线程能否获取threadLocal

public class ThreadTest {

    //线程私有,无法传递
    private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

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

        //线程1
        Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        new Thread(runnable2).start();

        try {
            
            Thread.sleep(1000);
            
            //主线程
            System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

结果:

Thread-0:threadLocal
Thread-1:null
main:null
  • 执行main方法的是父线程,它创建了两个子线程:线程0与线程1
  • 线程0赋予ThreadLocal一个值,则为线程0私有,其他线程拿不到。

InheritableThreadLocal

线程私有,但可以由父线程传递给子线程。继承于ThreadLocal

示例:在主线程中为threadLocalInheritableThreadLocal赋值,看哪个能被其他线程获取

//线程私有,无法传递
private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

//线程私有,但可以由父线程传递给子线程
private final static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();


public static void main(String[] args) {

    inheritableThreadLocal.set("inheritableThreadLocal");
    threadLocal.set("threadLocal");

    //线程0,输出threadLocal
    Runnable runnable1 = () -> {
        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
    };
    new Thread(runnable1).start();

    //线程1,输出inheritableThreadLocal
    Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());
    new Thread(runnable2).start();

    
    try {

        Thread.sleep(1000);
        //主线程
        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());

    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
}

结果:

Thread-0:null
Thread-1:inheritableThreadLocal
main:threadLocal
main:inheritableThreadLocal     
  • 因为ThreadLocal无法跨线程传递,所以线程0无法获取主线程赋值的ThreadLocal
  • 因为InheritableThreadLocal可以由父线程传递给子线程(线程0与线程1是在主线程创建的,所以主线程是他们的父线程),所以线程1可以获取InheritableThreadLocal
  • 因为ThreadLocal主线程赋值的,所以主线程可以获取
  • 因为InheritableThreadLocal主线程赋值的,所以主线程也可以获取,并可传递给子线程

但是InheritableThreadLocal有一个问题

在使用线程池时,线程可能会被重用。比如:

  1. 父线程赋予InheritableThreadLocal一个值,并从线程池拿出一些线程作为子线程运行任务,使用了这个值;
  2. 在中途,父线程修改了InheritableThreadLocal的值,再从线程池拿出一些线程运行任务,由于线程池复用,此时的子线程可能是1中释放的,所以用到的可能不是新修改的InheritableThreadLocal值。

执行以下代码

创建一个线程池,设置其固定大小为1,调用这个线程池两次,在此之前分别对主线程中的InheritableThreadLocal进行赋值操作,观察运行的结果。

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

public class ThreadTest {

    public static InheritableThreadLocal<String> inheritableThreadlocal = new InheritableThreadLocal<>();

    private static ExecutorService executorservice = Executors.newFixedThreadPool(1);


    public static void main(String args[]) throws Exception{
        
        inheritableThreadlocal.set("主线程赋值1");

        executorservice.submit(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-01获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+inheritableThreadlocal.get());
            }
        });


        Thread.sleep(1000);


        inheritableThreadlocal.set("主线程赋值2");
        executorservice.submit(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-02获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+inheritableThreadlocal.get());
            }
        });
        
        executorservice.shutdown( );       
    }
}

结果:

子线程-01获取本地变量值: pool-1-thread-1::主线程赋值1
子线程-02获取本地变量值: pool-1-thread-1::主线程赋值1

两次调用获取的值一致,子线程-02并没有获取到inheritableThreadlocal的新值。

这是因为线程池是复用线程的,第一个线程01结束后,创建的02线程是对01的复用,从名字也可以看出,二者是同一个线程,而01得到了从父线程传递的inheritableThreadlocal为**“主线程赋值1”**,所以02获取的与01是同一个值。要解决此问题,需要使用TransmittableThreadLocal

TransmittableThreadLocal

阿里出品,继承于InheritableThreadLocal,解决在线程池复用线程导致的上下文相同问题

代码示例:

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>transmittable-thread-local</artifactId>
	<version>2.14.2</version>
</dependency>

需要注意的是,在使用TransmittableThreadLocal时,我们需要使用com.alibaba.ttl.TtlRunnablecom.alibaba.ttl.TtlCallable来对任务进行包装,以确保线程池中的线程能正确传递ThreadLocal的值。

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadTest {
    
    private static ExecutorService executorservice = Executors.newFixedThreadPool(1);
    
    private static TransmittableThreadLocal<String> transmittableThreadLocal  = new TransmittableThreadLocal<String>();
    
    public static void main(String args[]) throws Exception{

        transmittableThreadLocal.set("主线程赋值1");

        executorservice.submit(TtlRunnable.get(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-01获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+transmittableThreadLocal.get());
            }
        }
        ));
        
        Thread.sleep(1000);

        transmittableThreadLocal.set("主线程赋值2");
        executorservice.submit(TtlRunnable.get(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-02获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+transmittableThreadLocal.get());
            }
                                               }
        ));

        executorservice.shutdown( );
    }
}

结果:

子线程-01获取本地变量值: pool-1-thread-1::主线程赋值1
子线程-02获取本地变量值: pool-1-thread-1::主线程赋值2

SpringBoot应用实例

1.线程池

上面使用了TtlRunnableTtlCallable,这里不用,但需要配置TtlExecutors包装线程池。

为了展现复用场景,将所有参数设置为1

import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "asyncExecutor")
    public Executor asyncExecutor() {
        
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        executor.setCorePoolSize(1); // 核心线程数1
        
        executor.setMaxPoolSize(1); // 最大线程数1
       
        executor.setQueueCapacity(1); // 队列容量1,如果此数字大于0,使用队列LinkedBlockingQueue
        
        executor.setThreadNamePrefix("AsyncThread-"); // 线程名称前缀
        
        executor.initialize();//初始化线程池,是以上参数生效,最好放到最后
        
        //需要使用ttl线程池包装,否则`TransmittableThreadLocal`不生效
        return TtlExecutors.getTtlExecutorService(executor.getThreadPoolExecutor());
        
    }
    
}

com.alibaba.ttl.threadpool.TtlExecutorsThreadPoolTaskExecutor区别

功能和目的:

  • TtlExecutors: com.alibaba.ttl.threadpool.TtlExecutors是阿里巴巴开源的一个库,用于在多线程环境中传递ThreadLocal变量。它提供了一系列的静态方法,可以将原始的线程池包装为一个支持ThreadLocal传递的线程池。
  • ThreadPoolTaskExecutor: ThreadPoolTaskExecutor是Spring框架提供的一个实现了ExecutorAsyncTaskExecutor接口的线程池类。它提供了更多的功能和配置选项,例如线程池的大小、拒绝策略、线程池的初始化和销毁等。

支持的功能:

  • TtlExecutors: TtlExecutors主要用于解决在多线程环境下,使用ThreadLocal会导致值在线程池中传递不正确的问题。它通过包装原始的线程池,确保在任务执行时,线程池中的ThreadLocal值能正确地传递。
  • ThreadPoolTaskExecutor: ThreadPoolTaskExecutor提供了更多的功能和配置选项,例如线程池大小的调整、任务队列的选择、拒绝策略的配置等。

使用场景:

  • TtlExecutors: TtlExecutors适用于在多线程环境下,需要在线程池中传递ThreadLocal值的场景,例如在跨线程执行任务时,需要保持一些上下文信息的一致性。
  • ThreadPoolTaskExecutor: ThreadPoolTaskExecutor适用于常规的线程池管理场景,可以配置线程池的各种属性和行为。

需要根据具体的需求来选择适合的线程池管理类。如果需要在多线程环境下传递ThreadLocal值,可以考虑使用TtlExecutors。如果需要更多的功能和配置选项,可以使用ThreadPoolTaskExecutor

2.ThreadLocal工具类

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>transmittable-thread-local</artifactId>
	<version>2.14.2</version>
</dependency>
import com.alibaba.ttl.TransmittableThreadLocal;

public class ThreadTool {
    
    //清除threadlocal中的值
    public static void clear() {
        threadlocal.remove();
    }

    public static String getThreadlocal() {
        return threadlocal.get();
    }

    public static void setThreadlocal(String threadlocalParam) {
        threadlocal.set(threadlocalParam);
    }

    public static TransmittableThreadLocal<String> threadlocal = new TransmittableThreadLocal<String>();

}

3.异步方法

注意@Async注解或有多种失效情况,要避免;

指定@Async异步使用上面配置的线程池,括号中添加线程池名字。

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class TtlTool {

    @Async("asyncExecutor")
    public void ttl1(Integer i) throws InterruptedException{
        
        System.out.println("第"+i+"个异步方开始法执行");
        
        System.out.println("第"+i+"个异步方法的线程:"+Thread.currentThread().getName()+" 的threadlocal值为:" + ThreadTool.getThreadlocal());

        //ThreadTool.clear();

        System.out.println();
        System.out.println();
    }
}

4.Controller调用

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private TtlTool ttlTool;

    //localhost:8080/user/ttl
    @RequestMapping("/ttl")
    public void ttl() throws InterruptedException{
		
        
        ThreadTool.setThreadlocal("threadlocal1");
       
        System.out.println(Thread.currentThread().getName()+" : " + ThreadTool.getThreadlocal());
        
        //第一次执行异步
        int i = 1;
        ttlTool.ttl1(i);
		
        //等3秒,让上一个线程执行完毕后,释放回线程池,以便在下面复用
        Thread.sleep(3000);
		
        //中途修改父线程threadlocal值
        ThreadTool.setThreadlocal("threadlocal2");
        System.out.println(Thread.currentThread().getName()+" : " + ThreadTool.getThreadlocal());

		//第二次执行异步
        i = 2;
        ttlTool.ttl1(i);

    }

}

5.执行结果

http-nio-8080-exec-3 : threadlocal1
第1个异步方开始法执行
第1个异步方法的线程:AsyncThread-1 的threadlocal值为:threadlocal1

http-nio-8080-exec-3 : threadlocal2
第2个异步方开始法执行
第2个异步方法的线程:AsyncThread-1 的threadlocal值为:threadlocal2

结果分析:

父线程

1.打印 :

http-nio-8080-exec-3 : threadlocal1

http-nio-8080-exec-3 : threadlocal2

一是证明http-nio-8080-exec-3是执行Controller方法的线程,二是证明证明父线程的threadlocal值修改成功。

2.打印 :

第1个异步方法的线程:`AsyncThread-1 的threadlocal`值为:threadlocal1

此打印结果证明执行Controller方法的线程http-nio-8080-exec-3是父线程,因为threadlocal1是在Controller方法中设置的,却被异步方法得到,证明存在父子线程传递,所以http-nio-8080-exec-3是父线程,被调用的异步方法执行者是子线程

2.执行过程

收到请求后,开启一个线程先执行Controller方法,并设置threadlocal值为threadlocal1。

然后执行第一个异步任务,因为异步任务是在Controller方法中调用的,所以执行Controller方法的线程是异步任务的父线程,则threadlocal1传递到了第一个异步任务,打印结果:

第1个异步方法的线程:`AsyncThread-1 的threadlocal`值为:threadlocal1

第一个异步任务执行完后,等待3秒,执行该异步任务的线程会回归线程池,此时修改父线程的threadlocal值为threadlocal2,在执行第二个异步任务,打印结果:

第2个异步方法的线程:AsyncThread-1 的threadlocal值为:threadlocal2

两个打印结果的线程名相同,都是AsyncThread-1,证明确实复用了一个线程。

threadlocal1threadlocal2的不同值,证明TransmittableThreadLocal成功解决了在线程池复用线程导致的上下文相同问题

ThreadLocalRandom

多线程环境下生成随机数

原Random的使用:

public class RandomTest {

    public static void main(String[] args) {
        // 创建一个默认种子的随机数生成器
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // random.nextInt(5) 输出一个[0,5)之间的随机数
            System.out.println(random.nextInt(5));
        }
    }
}

每个Random实例里面都有一个原子性种子变量用来记录当前种子值。在多线程下使用单个Random实例生成随机数时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以 ThreadLocalRandom 应运而生。

ThreadLocalRandom的使用:

在多线程环境下使用

import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomTest {

    public static void main(String[] args) {
        // 获取一个随机数生成器
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            // threadLocalRandom.nextInt(5) 输出一个[0,5)之间的随机数
            System.out.println(threadLocalRandom.nextInt(5));
        }
    }
}

使用线程池的ThreadLocal传递问题

如何解决线程池中的值传递功能?

  • 多个线程之间竞争同一个变量,为了线程安全进行值隔离,可以使用ThreadLocal
  • 父子线程之间的值传递,可以使用InheritableThreadLocal类来实现。

在遇到线程池等会池化复用线程的执行组件情况下,子父线程上下文会重复,上述两种方案都会失灵。

解决方案一

通过TransmittableThreadLocal类 + TtlRunnable来实现。

TtlRunnable是阿里巴巴开源的com.alibaba.ttl.threadpool.TtlRunnable类的简称。它是一个用于在多线程环境中传递ThreadLocal变量的Runnable的包装类。

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>transmittable-thread-local</artifactId>
	<version>2.14.2</version>
</dependency>
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadTest {
    
    private static ExecutorService executorservice = Executors.newFixedThreadPool(1);
    
    private static TransmittableThreadLocal<String> transmittableThreadLocal  = new TransmittableThreadLocal<String>();
    
    public static void main(String args[]) throws Exception{

        transmittableThreadLocal.set("主线程赋值1");

        executorservice.submit(TtlRunnable.get(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-01获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+transmittableThreadLocal.get());
            }
        }
        ));
        
        Thread.sleep(1000);

        transmittableThreadLocal.set("主线程赋值2");
        executorservice.submit(TtlRunnable.get(new Runnable(){
            @Override
            public void run() {
                System.out.println("子线程-02获取本地变量值: "
                        + Thread.currentThread().getName()+"::"+transmittableThreadLocal.get());
            }
                                               }
        ));

        executorservice.shutdown( );
    }
}

结果:

子线程-01获取本地变量值: pool-1-thread-1::主线程赋值1
子线程-02获取本地变量值: pool-1-thread-1::主线程赋值2

解决方案二

通过TransmittableThreadLocal类 + TtlCallable来实现。

TtlCallable是阿里巴巴开源的com.alibaba.ttl.threadpool.TtlCallable类的简称。它是一个用于在多线程环境中传递ThreadLocal变量的Callable的包装类。

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>transmittable-thread-local</artifactId>
	<version>2.14.2</version>
</dependency>
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlCallable;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadTest {

    private static ExecutorService executorservice = Executors.newFixedThreadPool(1);

    private static TransmittableThreadLocal<String> transmittableThreadLocal  = new TransmittableThreadLocal<String>();

    public static void main(String args[]) throws Exception{

        transmittableThreadLocal.set("set parent value");
		
        //用TtlCallable包装Callable
        Callable<String> ttlCallable1 = TtlCallable.get(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //读取父线程中的值,其值为:set parent value
                System.out.println("线程1 "+transmittableThreadLocal.get());
                return "call方法返回值";
            }
        });

        Future<String> future = executorservice.submit(ttlCallable1);
        //读取当前线程中的值,其值为:set parent value
        System.out.println("父线程 "+transmittableThreadLocal.get());

        Thread.sleep(1000);

        Callable<String> ttlCallable2 = TtlCallable.get(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //读取父线程中的值,其值为:set parent value
                System.out.println("线程2 "+transmittableThreadLocal.get());
                return "call方法返回值";
            }
        });

        Future<String> futuree = executorservice.submit(ttlCallable2);
        //读取当前线程中的值,其值为:set parent value
        System.out.println("父线程 "+transmittableThreadLocal.get());

        executorservice.shutdown();
    }
}

结果:

父线程 set parent value
线程1 set parent value
父线程 set parent value
线程2 set parent value

解决方案三

通过TransmittableThreadLocal类 + TtlExecutors来实现。

这种不需要使用TtlRunnableTtlCallable

com.alibaba.ttl.threadpool.TtlExecutors是阿里巴巴开源的一个库,用于在多线程环境中传递ThreadLocal变量。它提供了一系列的静态方法,可以将原始的线程池包装为一个支持ThreadLocal传递的线程池。

import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.alibaba.ttl.threadpool.TtlExecutors;


public class ThreadTest {

    
    public static TransmittableThreadLocal<String> threadlocal = new TransmittableThreadLocal<String>();


    public static void main(String[] args) throws InterruptedException{

        // 创建一个普通的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 使用TtlExecutors包装线程池
        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);
        
        threadlocal.set("threadlocal1");
        // 提交一个任务
        ttlExecutorService.submit(new Runnable() {
            @Override
            public void run() {

                System.out.println("线程1 " + threadlocal.get());
            }
        });


        Thread.sleep(1000);

        threadlocal.set("threadlocal2");
        // 提交一个任务
        ttlExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                //threadlocal.set("threadlocal2");
                System.out.println("线程2 " + threadlocal.get());

            }
        });

        // 关闭线程池
        ttlExecutorService.shutdown();
    }
}
  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值