关于在 Spring 单例范围服务中使用 ThreadLocal 的问题

Questions about using ThreadLocal in a Spring singleton scoped service

在我下面的单例范围服务 class 中,class 中的所有方法都需要一些在调用 Service.doA() 时已知的用户上下文。我没有跨方法传递信息,而是考虑将这些值存储在 TheadLocal 中。我对这种方法有两个问题:

1) 下面的实现是否正确使用了 ThreadLocal?也就是说,它是线程安全的,正确的值将是 read/written 到 ThreadLocal?

2) ThreadLocal userInfo 是否需要明确清理以防止任何内存泄漏?它会被垃圾收集吗?

@Service
public class Service {
    private static final ThreadLocal<UserInfo> userInfo = new ThreadLocal<>(); 

    public void doA() {
        // finds user info
        userInfo.set(new UserInfo(userId, name));
        doB();
        doC();
    }

    private void doB() {
        // needs user info
        UserInfo userInfo = userInfo.get();
    }

    private void doC() {
        // needs user info
        UserInfo userInfo = userInfo.get();
    }
}

用完一定要清理干净。 ThreadLocals 非常容易泄漏内存,堆内存和 permgen/metaspace 内存通过 classloders 泄漏。在您的情况下,最好的方法是:

public void doA() {
  // finds user info
  userInfo.set(new UserInfo(userId, name));
  try {
    doB();
    doC();
  } finally {
    userInfo.remove()
  }
}

1) 示例代码没问题,除了 doB 和 doC 中的名称冲突,其中您对引用 ThreadLocal 的静态变量使用相同的名称,就像您对保存从ThreadLocal.

2) 您存储在 ThreadLocal 中的对象会一直附加到该线程,直到被显式删除。如果您的服务在 servlet 容器中执行,例如,当请求完成时,它的线程 returns 进入池。如果您还没有清理线程的 ThreadLocal 变量内容,那么该数据将一直存在,以伴随线程为下一个分配的任何请求。每个线程都是一个 GC 根,附加到线程的线程局部变量在线程死后才会被垃圾回收。根据the API doc:

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

如果您的上下文信息仅限于一项服务的范围,您最好通过参数传递信息而不是使用 ThreadLocal。 ThreadLocal 适用于需要在不同服务或不同层中提供信息的情况,如果仅由一项服务使用,您的代码似乎只会过于复杂。现在,如果您有 AOP 建议在不同的不同对象上使用的数据,则将该数据放在线程本地中可能是一种有效的用法。

要执行清理,通常您会确定线程完成当前处理的点,例如在 servlet 过滤器中,线程局部变量可以在线程返回到线程池之前被删除。您不会使用 try-finally 块,因为插入 threadlocal 对象的地方离您清理它的地方很远。

当您使用 ThreadLocal 时,无论发生什么,您都需要确保清理它,因为:

  1. 它会以某种方式造成内存泄漏,因为 GC 无法收集该值,因为当且仅当不再有对象直接或间接硬引用该对象时,该对象才符合 GC 的条件。因此,例如,您的 ThreadLocal 实例通过其内部 ThreadLocalMap 间接地硬引用了您的值,摆脱这种硬引用的唯一方法是调用 ThreadLocalMap#remove()ThreadLocalMap 中删除值。使您的值符合 GC 条件的另一种可能方法是,您的 ThreadLocal 实例本身符合 GC 条件,但这里它在 class Service 中是一个常量,所以它永远不会有资格获得我们想要的 GC。所以唯一预期的方法是调用 ThreadLocalMap#remove().
  2. 它会产生很难发现的错误,因为大多数时候使用您的 ThreadLocal 的线程是 thread pool 的一部分,这样线程将被重新用于另一个请求,所以如果您的 ThreadLocal 没有被正确清理,线程将重用存储在 ThreadLocal 中的对象实例,它甚至可能与导致复杂错误的新请求无关。例如,这里我们可以得到不同用户的结果,只是因为 ThreadLocal 还没有被清理。

所以模式如下:

try {
    userInfo.set(new UserInfo(userId, name));
    // Some code here 
} finally {
    // Clean up your thread local whatever happens
    userInfo.remove();
}

关于线程安全,虽然UserInfo不是线程安全的,但它当然是线程安全的,因为每个线程都会使用自己的UserInfo实例,所以不会存储UserInfo的实例进入 ThreadLocal 将被多个线程访问或修改,因为 ThreadLocal 值以某种方式限定了当前线程的范围。