一、ThreadLocal的基本使用及区别-粗略源码了解
1.了解
在JAVA中线程之间传输数据的方式有多种,而本文主要探讨ThreadLocal及其衍生类的使用场景以及实际业务中的使用。
2.使用场景
- 业务系统的参数传递:在我们的业务系统中可能会用到许多公共参数,可能是用户的token信息,或者用户id,在我们链路中可能某一个方法需要用到它,那么我们又不想一层层的传递它。
3.基本了解
0.代码开始准备
1.首先需要在启动类中加上**@EnableScheduling**注解来开启定时任务
2.了解@Scheduled(fixedDelay = 1000 ) //每一秒运行一次
3.导入依赖transmittable-thread-local
dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
1.ThreadLocal
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
package com.wm.file.threadtest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author jxj
* @description
* @create 2024/1/9 16:49
*/
@Component
public class threadLocalTest {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
@Scheduled(fixedDelay = 2000)
public void threadLocalTest() {
CONTEXT_HOLDER.set("initial");
// 线程1
new Thread(() -> {
CONTEXT_HOLDER.set("thread1");
System.out.println("thread1 threadLocal=====" + CONTEXT_HOLDER.get());
}).start();
// 线程2
new Thread(() -> System.out.println("thread2 threadLocal=====" + CONTEXT_HOLDER.get())).start();
// 线程3
new Thread(() -> {
System.out.println("thread3 threadLocal=====" + CONTEXT_HOLDER.get());
}).start();
}
}
执行结果:如下
源码分析:查看ThreadLocal的源码
1.1首先它的set方法会首先去获取当前线程,然后再调用getMap方法去获取当前线程的ThradLocalMap,这个线程中的一个本地变量默认为null。
1.2而这个ThreadLocalMap又是ThreadLocal中的一个静态内部类。回到上面的set方法,因为第一次此时的线程中的这个变量还未赋值,所以为null,于是调用createMap
1.3查看构造方法,实际上跟hashmap内部结构类似,也是一个Entry对象真正持有我们存入的value
1.4而这个Entry又是ThreadLocalMap中的一个静态内部类
2.我们再查看一下get方法
2.1我们可以看到同样是先获取当前线程对象,然后再获取它所持有的ThreadLocalMap,然后根据threadLocal对象为key找到实际上持有数据的Entry
2.2所以说我们可以看到实际上threadLocal对象只是作为了一个key而真正存储数据的是每个线程自身的thread内持有的一个ThreadLocalMap的对象,而我们的thread1、thread2、thread3线程自然就不能获取到数据。
自此我们简单的介绍了ThreadLocal的用法及其get set的原理,但是还没完,ThreadLocal对象提供的一个remove方法是做什么的。为什么我们在结束的时候需要手动去调用remove方法呢?
结论先行:如果使用后不remove可能会有内存泄漏的风险!
通过执行结果可以看出ThreadLocal变量无法由父(主)线程传递给子线程。由此引入InheritableThreadLocal
2.InheritableThreadLocal
代码:
package com.wm.file.threadtest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author jxj
* @description
* @create 2024/1/9 16:49
*/
@Component
public class InheritableThreadLocalTest {
private static final InheritableThreadLocal<String> CONTEXT_HOLDER1 = new InheritableThreadLocal<>();
// 线程池大小为1 保证后续执行线程为已初始化的线程
private static ExecutorService ioExecutor = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.MINUTES, new ArrayBlockingQueue<>(50), new ThreadPoolExecutor.CallerRunsPolicy());
@Scheduled(fixedDelay = 2000)
public void test() {
CONTEXT_HOLDER1.set("main thread");
System.out.println("main thread threadLocal=====" + CONTEXT_HOLDER1.get());
// (线程池)子线程
ioExecutor.execute(() -> {
System.out.println("child thread threadLocal=====" + CONTEXT_HOLDER1.get());
CONTEXT_HOLDER1.set("clear thredlocal");
});
}
}
如图:
inheritableThreadLocal变量主要解决ThreadLocal变量无法进行父子线程传递的问题
工作中实际问题:
Shiro+线程池的时候获,当前用户的相关信息存储在session中。记录操作信息,系统会从session中获取当前操作的操作人,存入数据库。操作人员在登录多个账号检测日志生成情况时发现我完成的功能偶尔会出现 A用户
进行了操作,但日志中记录的确是用户B
情况发生。
导致问题出现的原因:
shiro使用了可继承父类的ThreadLocal变量,来保证线程间的数据隔离。然而在使用线程池时,各个线程是可复用的,就导致ThreadLocal变量只在创建线程时生成了一份,后续使用该线程的所有流程都使用的是创建线程时生成的ThreadLocal变量,即A用户操作时可能会获取到B用户创建的操作线程,从而获取到B用户的信息。
解决方案:(我这里采用的shiro官方推荐的方案)
1.不使用线程池
2.shiro提供的associateWith(runnable)方法
package com.wdjd.business.config.ThreadPool;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectRunnable;
import org.apache.shiro.util.ThreadContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Objects;
/**
* @description Shiro+线程池 用户幸信息错乱问题解决方案
* @author jxj
* @param[1] null
* @time 2024/1/12 10:32
*/
public class ShiroSubjectAwareTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public boolean prefersShortLivedTasks() {
return false;
}
@Override
public void execute(Runnable task) {
if (task instanceof SubjectRunnable) {
super.execute(task);
return;
}
// not SubjectRunnable and currentSubject not null
Subject currentSubject = ThreadContext.getSubject();
if (Objects.nonNull(currentSubject)) {
//这里处理 currentSubject.associateWith(task)
super.execute(currentSubject.associateWith(task));
} else {
super.execute(task);
}
}
}
3.使用别的安全框架 比如 Srping Security
4.大神之路 重写Shiro 的 ThreadContext
3.TransmittableThreadLocal
代码:
package com.wm.file.threadtest;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
public class TransmittableThreadLocalTest {
private static final TransmittableThreadLocal<String> CONTEXT_HOLDER2 = new TransmittableThreadLocal<>();
// 线程池大小为1 保证后续线程为已初始化的线程
private static ExecutorService ioExecutor1 = new ThreadPoolExecutor(1, 1, 10L, TimeUnit.MINUTES, new ArrayBlockingQueue<>(50), new ThreadPoolExecutor.CallerRunsPolicy());
@Scheduled(fixedDelay = 1000)
public void test1() {
CONTEXT_HOLDER2.set("main thread");
System.out.println("main thread threadLocal=====" + CONTEXT_HOLDER2.get());
// (线程池)子线程 注意提交需要使用TtlRunnable
ioExecutor1.execute(TtlRunnable.get(() -> {
System.out.println("child thread threadLocal=====" + CONTEXT_HOLDER2.get());
CONTEXT_HOLDER2.set("clear thredlocal");
}));
// (线程池)子线程 注意提交需要使用TtlRunnable
ioExecutor1.execute(TtlRunnable.get(() -> {
System.out.println("child1 thread threadLocal=====" + CONTEXT_HOLDER2.get());
CONTEXT_HOLDER2.set("clear1 thredlocal");
}));
}
}
如图:
TransmittableThreadLocal变量在子线程(线程池复用线程)启动时,都会将主线程变量传递至子线程
有问题欢迎大家评论区留言 或者加qq:1435469553@qq.com