如何根据Sonar清理Java ThreadLocals?

How to clean up Java ThreadLocals in accordance with Sonar?

A Sonar rule available since Aug 21, 2019 (squid:S5164 / RSPEC-5164) 要求在不再使用时清理 "ThreadLocal" 变量。因此,让我们采用以下 class(JDK6 兼容):

public class ThreadLocalExample {

    private static final ThreadLocal<NumberFormat> formats = new ThreadLocal<NumberFormat>() {
        @Override
        protected NumberFormat initialValue() {
            final NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);
            nf.setMinimumFractionDigits(2);
            nf.setMaximumFractionDigits(2);
            nf.setGroupingUsed(false);
            return nf;
        }
    };

    public static NumberFormat getFormatter() {
        return formats.get();
    }
}

Sonar 在 ThreadLocal 声明中报告了一个 主要错误 ,解释如下:

"ThreadLocal" variables should be cleaned up when no longer used

ThreadLocal variables are supposed to be garbage collected once the holding thread is no longer alive. Memory leaks can occur when holding threads are re-used which is the case on application servers using pool of threads.

To avoid such problems, it is recommended to always clean up ThreadLocal variables using the remove() method to remove the current thread’s value for the ThreadLocal variable.

现在,我采用了 ThreadLocal 方法,以便尽可能地重用 NumberFormat 个实例,避免每次调用创建一个实例,所以我认为如果我调用 remove() 在代码的某处,我将失去此解决方案的所有优势。我错过了什么吗?非常感谢。

您应该在使用格式化程序后直接调用#remove。正如您所写,那样做会破坏目的。

您只需在以下情况下调用#remove。您的 Web 应用程序已从应用程序服务器卸载,例如Tomcat。但是应用服务器本身一直在 运行.

在这种情况下,应用程序服务器可能会保留它为您的应用程序创建的线程,并且这些线程仍将各自拥有一个 NumberFormat 实例与之关联。 那是你的内存泄漏

所以如果你总是重启整个应用服务器,你可能不需要关心这个问题。

如果您想正确清理 ThreadLocal,您需要在应用程序开始关闭时调用 #remove。通过这种方式,您最多可以重复使用 NumberFormat 实例,同时仍能正确清理。

声纳就在这里。

每个线程都有自己的 ThreadLocal 状态,因此有自己的 NumberFormat 实例。
所以在一般情况下,不清除状态中的数据可能是不可取的,因为线程可能会被重用(由服务器回收)并且前一个客户端的状态值可能与当前客户端不一致。
例如,某些客户端的格式可能为 US,其他客户端的格式可能为 FR,等等... 除了一些线程可以实例化 ThreadLocal class,其他的没有。但是通过不清理状态,状态将仍然为可能不需要它们的线程使用内存。

好吧,在您的代码中,ThreadLocal 状态没有可变性,因为您为任何实例设置了状态,因此不可能存在不一致的风险,只有内存 "waste"。

Now, I adopted the ThreadLocal approach in order to reuse NumberFormat instances as much as possible, avoiding the creation of one instance per call

您根据线程请求重用 ThreadLocal 状态。
所以如果你有 50 个线程,你就有 50 个状态。
在 Web 应用程序中,服务器将客户端 HTTP 请求映射到一个线程。
因此,您不会仅在 1 个 http 请求的范围内创建格式化程序的多个实例。这意味着如果您通过请求处理使用格式化程序一两次,ThreadLocal 缓存不会带来很大的价值。但是用多了,用起来就有意义了。

so I think if I called remove() somewhere in the code, I would lose all the advantages of this solution

如果在请求处理完成后调用 remove(),则不会影响性能。您不会失去任何优势,因为您可能会在请求范围内多次使用格式化程序,并且只会在最后清理它。

您在 servlet 规范中有请求侦听器: https://docs.oracle.com/javaee/7/api/javax/servlet/ServletRequestListener.html.
你可以在 void requestDestroyed(ServletRequestEvent sre).

中做到这一点

您提供的 link 说明如下:

Memory leaks can occur when holding threads are re-used which is the case on application servers using pool of threads.

例如,您构建了一个包含 7 个线程的池,因为您的 CPU 有 8 个内核。然后你向池中提交一个任务,它会被其中一个线程解决。之后线程可能没有其他任务要做,但线程仍然持有 ThreadLocal 对象。这将是一种内存浪费,并且由于 ThreadLocal 具有对包含对象的引用,因此垃圾收集器无法删除包含对象(可能导致内存泄漏)。

如果不重用线程并清除对线程的所有引用,则不会发生内存泄漏。如果稍后重用该线程,threadLocal 对象将在内存中,直到它被覆盖或清理或直到线程被销毁。