ThreadLocal 知识储备传送门:
ThreadLocal 原理及源码详解
ThreadLocal 内存泄漏和常见问题详解
什么是ThreadLocal?
ThreadLocal 是一种提供线程本地变量(也称为线程局部变量)的类,这种变量确保了在不同的线程中访问同一个 ThreadLocal 变量时,每个线程会有一个该变量的私有副本,即使多个线程修改了相同的 ThreadLocal 变量,每个线程所访问的副本仍然是独立的,从而避免了多线程环境下共享变量可能导致的数据不一致和竞争问题。
ThreadLocal 的作用?
ThreadLocal 的主要作用在于线程隔离,ThreadLocal 中的数据只属于当前线程,对其他线程是不可见的,实现每一个线程都有自己线程本地变量,ThreadLocal 贯穿了线程的整个生命周期,在线程的生命周期中的任何地方都可以取出,这样这些本地变量就类似是全局变量了,也可以说 ThreadLocal 最大的用处就是把局部变量共享成全局变量。
既然 ThreadLocal 只是给每个线程创建了变量副本,方便变量副本在线程生命周期中的多个方法中使用,那我们 new 一下不就可以了吗,为什么还要使用 ThreadLocal ?
没错,如果不嫌麻烦,new 一个变量副本作为参数传递是完全可以的,但是对用这种全局变量,使用 ThreadLocal 来维护会大大降低维护成本。
ThreadLocal 的使用场景?
我们熟知的 Spring 框架中就大量使用了 ThreadLocal ,如下:
//我们非常熟悉的 spring 事务管理器
public abstract class TransactionSynchronizationManager {private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");public TransactionSynchronizationManager() {}}public abstract class RequestContextHolder {private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");public RequestContextHolder() {}}
ThreadLocal 的使用场景
- 场景一:每个线程需要独享一个局部对象(非常典型的 SimpleDateFormat 问题 )。
- 场景二:每个线程内需要保存全局变量,即一个变量需要在线程整个生命周期中的不同方法直接使用,避免参数传递(非常常见的用户信息传递问题 )。
场景一:每个线程需要独享一个局部对象
我们知道多线程的情况下,多个线程使用同一个 SimpleDateFormat 对象会有线程安全问题。
演示代码如下:
public class ThreadLocalTest {//同一个 SimpleDateFormat对象private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static ExecutorService threadPool = Executors.newFixedThreadPool(100);public static void main(String[] args) {for (int a = 0; a < 100; a++) {threadPool.execute(() -> {//synchronized (ThreadLocalTest.class){Date date = null;try {date = dateFormat.parse("2024-04-13 12:12:12");} catch (ParseException e) {e.printStackTrace();}System.out.println(date);//}});}}}
执行结果如下:
Exception in thread "pool-1-thread-6" Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-7" Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zt.zteam.main.controller.auth.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:27)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)
Exception in thread "pool-1-thread-13" java.lang.NumberFormatException: empty Stringat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zt.zteam.main.controller.auth.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:27)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zt.zteam.main.controller.auth.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:27)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zt.zteam.main.controller.auth.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:27)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zt.zteam.main.controller.auth.ThreadLocalTest.lambda$main$0(ThreadLocalTest.java:27)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)
很明显多线程并发的情况下,执行报错了。
当然上面的错误可以使用 synchronized 、ReentrantLock 可以实现,但是使用锁机制的话会让并行变为串行,一个个排队排队执行,会有性能问题。
synchronized 、ReentrantLock 传送门:
synchronized 使用及深入理解
深入理解 ReentrantLock 【源码分析】
下面我们改造上面的代码,使用 ThreadLocal 来实现,代码如下:
public class ThreadLocalTest {//ThreadLocalprivate static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};//线程池 方便发起多线程调用 真实业务开发不要这样使用线程池public static ExecutorService threadPool = Executors.newFixedThreadPool(100);public static void main(String[] args) {for (int a = 0; a < 100; a++) {threadPool.execute(() -> {Date date = null;try {date = dateFormatThreadLocal.get().parse("2024-04-13 12:12:12");} catch (ParseException e) {e.printStackTrace();}System.out.println(date);});}}
}
执行结果:
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
Sat Apr 13 12:12:12 CST 2024
.................................................
很明显,ThreadLocal 帮我们解决了 SimpleDateFormat 的问题。
场景二:每个线程内需要保存全局变量
当前线程的用户信息需要被线程内所有方法共享,也就是需要被线程内部的多个方法使用,比较直接也比较麻烦的解决方案是把用户信息一层层的传递下去,这显然不是一个好的解决方案,这里我们就可以使用 ThreadLocal 来解决了。
使用 ThreadLocal 来传递用户信息,思路很简单,我们在拦截器或者过滤器中对用户信息进行拦截处理后,设置到 ThreadLocal 中,后续的业务就可以使用用户信息了,伪代码如下:
UserContextHolder 类,对 ThreadLocal 稍加包装。
public class UserContextHolder {//ThreadLocal 对象private static final ThreadLocal<UserInfoVO> USER_THREAD_LOCAL = new ThreadLocal<>();/*** @Description: 设置用户信息* @Date: 2024/4/13 17:00*/public static void setUser(UserInfoVO userInfoVO) {USER_THREAD_LOCAL.set(userInfoVO);}/*** @Description: 获取用户信息* @Date: 2024/4/13 17:00*/public static UserInfoVO getUser() {return USER_THREAD_LOCAL.get();}/*** @Description: 获取用户姓名* @Date: 2024/4/13 17:00*/public static String getUsername() {return USER_THREAD_LOCAL.get().getName();}/*** @Description: 获取用户id* @Date: 2024/4/13 17:00*/public static Long getUserId() {return USER_THREAD_LOCAL.get().getId();}/*** @Description: 获取用户工号* @Date: 2024/4/13 17:00*/public static String getUserCode() {return USER_THREAD_LOCAL.get().getCode();}/*** 清空上下文*/public static void remove() {USER_THREAD_LOCAL.remove();}}
拦截器或者过滤器,这里以过滤器举例,伪代码如下:
public class MyAuthenticationFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {UserInfoVO userInfoVO = new UserInfoVO();userInfoVO.setId(1L);userInfoVO.setCode("000001");userInfoVO.setName("张三");//设置用户信息UserContextHolder.setUser(userInfoVO);filterChain.doFilter(servletRequest, servletResponse);}@Overridepublic void destroy() {//将ThreadLocal数据清空 UserContextHolder.remove();Filter.super.destroy();}
}
测试验证代码:
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/thread-local-test")@ApiOperation(httpMethod = "GET", value = "ThreadLocal测试", notes = "ThreadLocal测试")Result<UserInfoVO> threadLocalTest() {return ResultGenerator.genSuccessResult(UserContextHolder.getUser());}
}
测试结果:
{"code": 200,"message": "SUCCESS","data": {"id": 1,"code": "000001","name": "张三"}
}
根据结果可以知道达到了我们预期的结果,使用 ThreadLocal 传递用户信息十分简单方便,减少了参数传递,且代码结构十分清晰。
ThreadLocal 优缺点?
优点:
- 隔离性,ThreadLocal 为每个线程保存了变量副本,不同线程之间互相不影响,保证了数据在多线程环境中的隔离性。
- 减少同步开销,上面演示的 SimpleDateFormat 案例。
- 上下文的参数透传, 提高了代码清晰度,ThreadLocal 可以很方便的在一个请求中传递信息,无需显示的进行参数传递,上面演示的用户信息传递案例。
缺点:
- 有内存泄漏的风险,使用完 ThreadLocal 后要及时清除 ThreadLocal 中的数据。
- 不能作为长时间的存储介质,因为 ThreadLocal 的生命周期和当前线程一致,不适合做长期存储。
- 增加资源开销,每个线程都要创建一个独立的变量,如果线程数很多,就会增加资源开销。
- 不支持异步访问,在进行用户信息传递的时候,如果有异步线程,就会获取不到用户信息,不过这个也正常,因为 ThreadLocal 本身就是当前线程的,换了线程肯定是获取不到信息的。
如有错误的地方欢迎指出纠正。