阿里三面:说说线程封闭与ThreadLocal的关系(中)

简介: 阿里三面:说说线程封闭与ThreadLocal的关系(中)

三个重要方法:

  • set()
    如果没有set操作的ThreadLocal, 很容易引起脏数据问题
  • get()
    始终没有get操作的ThreadLocal对象是没有意义的
  • remove()
    如果没有remove操作,则容易引起内存泄漏
  • 如果ThreadLocal是非静态的,属于某个线程实例,那就失去了线程间共享的本质属性;

那么ThreadLocal到底有什么作用呢?

我们知道,局部变量在方法内各个代码块间进行传递,而类变量在类内方法间进行传递;

复杂的线程方法可能需要调用很多方法来实现某个功能,这时候用什么来传递线程内变量呢?

即ThreadLocal,它通常用于同一个线程内,跨类、跨方法传递数据;

如果没有ThreadLocal,那么相互之间的信息传递,势必要靠返回值和参数,这样无形之中,有些类甚至有些框架会互相耦合;

通过将Thread构造方法的最后一个参数设置为true,可以把当前线程的变量继续往下传递给它创建的子线程

public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
   this (group, target, name,  stackSize, null, inheritThreadLocals) ;
}

parent为其父线程

if (inheritThreadLocals && parent. inheritableThreadLocals != null)
      this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;

createlnheritedMap()其实就是调用ThreadLocalMap的私有构造方法来产生一个实例对象,把父线程中不为null的线程变量都拷贝过来

private ThreadLocalMap (ThreadLocalMap parentMap) {
    // table就是存储
    Entry[] parentTable = parentMap. table;
    int len = parentTable. length;
    setThreshold(len) ;
    table = new Entry[len];
    for (Entry e : parentTable) {
      if (e != null) {
        ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
        if (key != null) {
          object value = key. childValue(e.value) ;
          Entry c = new Entry(key, value) ;
          int h = key. threadLocalHashCode & (len - 1) ;
          while (table[h] != null)
            h = nextIndex(h, len) ;
          table[h] = C;
          size++;
        }
    }
}

很多场景下可通过ThreadLocal来透传全局上下文的;

比如用ThreadLocal来存储监控系统的某个标记位,暂且命名为traceld.

某次请求下所有的traceld都是一致的,以获得可以统一解析的日志文件;

但在实际开发过程中,发现子线程里的traceld为null,跟主线程的traceld并不一致,所以这就需要刚才说到的InheritableThreadLocal来解决父子线程之间共享线程变量的问题,使整个连接过程中的traceld一致。


示例代码如下

import org.apache.commons.lang3.StringUtils;
/**
 * @author sss
 * @date 2019/1/17
 */
public class RequestProcessTrace {
    private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
            = new InheritableThreadLocal<FullLinkContext>();
    public static FullLinkContext getContext() {
        FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        if (fullLinkContext == null) {
            FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        }
        return fullLinkContext;
    }
    private static class FullLinkContext {
        private String traceId;
        public String getTraceId() {
            if (StringUtils.isEmpty(traceId)) {
                FrameWork.startTrace(null, "JavaEdge");
                traceId = FrameWork.getTraceId();
            }
            return traceId;
        }
        public void setTraceId(String traceId) {
            this.traceId = traceId;
        }
    }
}

ThreadLocal的副作用

为了使线程安全地共享某个变量,JDK给出了ThreadLocal.

但ThreadLocal的主要问题是会产生脏数据和内存泄漏;

这两个问题通常是在线程池的线程中使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两是在线程池的线程中使用ThreadLocal 引发的,因为线程池有线程复用和内存常驻两个特点

脏数据

线程复用会产生脏数据。

由于线程池会重用 Thread 对象,与 Thread 绑定的静态属性 ThreadLocal 变量也会被重用。

如果在实现的线程run()方法中不显式调用remove()清理与线程相关的ThreadLocal信息,那么若下一个线程不调用set(),就可能get() 到重用的线程信息。包括ThreadLocal所关联的线程对象的value值。

脏读案例

比如,用户A下单后没有看到订单记录,而用户B却看到了用户A的订单记录。通过排查发现是由于 session 优化引发。

在原来的请求过程中,用户每次请求Server,都需要通过 sessionId 去缓存里查询用户的session信息,这样无疑增加了一次调用。

因此工程师决定采用某框架来缓存每个用户对应的SecurityContext,它封装了session 相关信息。优化后虽然会为每个用户新建一个 session 相关的上下文,但由于Threadlocal没有在线程处理结束时及时remove()。在高并发场景下,线程池中的线程可能会读取到上一个线程缓存的用户信息。


示例代码

image.png

输出结果

26.png

重用错误案例

生产环境中,有时获取到的用户信息是别人的。查看代码后,发现是使用了ThreadLocal缓存获取到的用户信息。

ThreadLocal适用于变量在线程间隔离,而在方法或类间共享的场景。

若用户信息的获取比较昂贵(比如从DB查询),则在ThreadLocal中缓存比较合适。

问题来了,为什么有时会出现用户信息错乱?

1.1 案例

使用Spring Boot创建一个Web应用程序,使用ThreadLocal存放一个Integer值,代表需要在线程中保存的用户信息,这个值初始是null。在业务逻辑中,我先从ThreadLocal获取一次值,然后把外部传入的参数设置到ThreadLocal中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。

image.png

固定思维认为,在设置用户信息前第一次获取的值始终是null,但要清楚程序运行在Tomcat,执行程序的线程是Tomcat的工作线程,其基于线程池

而线程池会重用固定线程,一旦线程重用,那么很可能首次从ThreadLocal获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal中的用户信息就是其他用户的信息

1.2 bug 重现

在配置文件设置Tomcat参数-工作线程池最大线程数设为1,这样始终是同一线程在处理请求:

server.tomcat.max-threads=1

先让用户1请求接口,第一、第二次获取到用户ID分别是null和1,符合预期

28.png

用户2请求接口,bug复现!第一、第二次获取到用户ID分别是1和2,显然第一次获取到了用户1的信息,因为Tomcat线程池重用了线程。两次请求线程都是同一线程:http-nio-45678-exec-1

29.png

写业务代码时,首先要理解代码会跑在什么线程上:

  • Tomcat服务器下跑的业务代码,本就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题
  • 线程创建较昂贵,所以Web服务器会使用线程池处理请求,线程会被重用。使用类似ThreadLocal工具存放数据时,需注意在代码运行完后,显式清空设置的数据。

1.3 解决方案

在finally代码块显式清除ThreadLocal中数据。即使新请求过来,使用了之前的线程,也不会获取到错误的用户信息。

修正后代码:

image.png

ThreadLocal利用独占资源的解决线程安全问题,若就是要资源在线程间共享怎么办?就需要用到线程安全的容器

使用了线程安全的并发工具,并不代表解决了所有线程安全问题。

目录
相关文章
|
5月前
|
存储 监控 安全
解锁ThreadLocal的问题集:如何规避多线程中的坑
解锁ThreadLocal的问题集:如何规避多线程中的坑
182 0
|
3月前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
3月前
|
安全 Java
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
|
3月前
|
存储 安全 Java
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
|
3月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
38 0
|
3月前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
31 0
|
5月前
|
Android开发
Android中的多线程及AsyncTask的引入,最终入职阿里
Android中的多线程及AsyncTask的引入,最终入职阿里
|
12天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
20天前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
87 10
spring多线程实现+合理设置最大线程数和核心线程数
|
28天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
51 15
一个Android App最少有几个线程?实现多线程的方式有哪些?