阿里三面:说说线程封闭与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利用独占资源的解决线程安全问题,若就是要资源在线程间共享怎么办?就需要用到线程安全的容器

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

目录
相关文章
|
10小时前
|
存储 Java 测试技术
ThreadLocal:线程专属的变量
ThreadLocal:线程专属的变量
42 0
|
10小时前
|
存储 Java 数据安全/隐私保护
【JUC】ThreadLocal 如何实现数据的线程隔离?
【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
|
10小时前
|
安全 Java
java中线程经常被问到ThreadLocal你懂吗?
java中线程经常被问到ThreadLocal你懂吗?
11 0
|
10小时前
|
Java 关系型数据库 MySQL
【数据库连接,线程,ThreadLocal三者之间的关系】
【数据库连接,线程,ThreadLocal三者之间的关系】
26 0
|
10小时前
|
存储 安全 Java
调用链跨线程传递 ThreadLocal 对象对比
说起本地线程专属变量,大家首先会想到的是 JDK 默认提供的 ThreadLocal,用来存储在整个链路中都需要访问的数据,并且是线程安全的。由于在落地全链路压测的过程中,一个基本并核心的功能需求是流量标记需要在整个链路中进行传递,那么线程上下文环境成为解决这个问题最合适的技术。
46 2
调用链跨线程传递 ThreadLocal 对象对比
|
10小时前
|
存储 安全 Java
多线程------ThreadLocal详解
多线程------ThreadLocal详解
|
10小时前
|
存储 安全 Python
什么是Python中的线程局部存储(Thread Local Storage)?
【2月更文挑战第3天】【2月更文挑战第6篇】
|
10小时前
|
Java API 开发者
高逼格面试:线程封闭,新名词√
高逼格面试:线程封闭,新名词√
35 0
|
10小时前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
14 1
|
10小时前
|
设计模式 消息中间件 安全
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
11 0