阿里三面:说说线程封闭与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()。在高并发场景下,线程池中的线程可能会读取到上一个线程缓存的用户信息。


示例代码

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

输出结果

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

重用错误案例

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

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

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

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

1.1 案例

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

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

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

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

1.2 bug 重现

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

server.tomcat.max-threads=1

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

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

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

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

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

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


下一篇:ASP.NET WEB API 如何使用基于Post的方式传递多个值(二)