【问题标题】:How to clean up Java ThreadLocals in accordance with Sonar?如何按照 Sonar 清理 Java ThreadLocals?
【发布时间】:2020-01-05 00:47:31
【问题描述】:

自 2019 年 8 月 21 日起提供的 Sonar 规则 (squid:S5164 / RSPEC-5164) 要求在不再使用时清理“ThreadLocal”变量。所以,让我们采用以下类(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”变量不再使用时应清理

ThreadLocal 变量应该被垃圾回收一次 持有线程不再活着。持有时可能会发生内存泄漏 线程被重用,这在应用程序服务器上使用 线程池。

为避免此类问题,建议经常清理 ThreadLocal 变量使用remove() 方法删除当前 ThreadLocal 变量的线程值。

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

【问题讨论】:

  • 您是否真正分析过使用ThreadLocal 和简单地创建一个新实例并在每次调用getFormatter 时返回它之间是否存在差异?如果您要这样做,那么推理实例的生命周期似乎要容易得多。

标签: java memory-leaks sonarqube number-formatting thread-local


【解决方案1】:

声纳就在这里。

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

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

现在,我采用了 ThreadLocal 方法来重用 NumberFormat 尽可能多地创建实例,避免创建一个实例 每次通话

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

所以我想如果我在代码中的某处调用 remove(),我 将失去此解决方案的所有优势

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

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

【讨论】:

    【解决方案2】:

    您提供的link 内容如下:

    在重新使用持有线程时可能会发生内存泄漏,在使用线程池的应用程序服务器上就是这种情况。

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

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

    【讨论】:

      【解决方案3】:

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

      以下情况只需要拨打#remove即可。您的 Web 应用程序已从应用程序服务器卸载,例如雄猫。但应用服务器本身继续运行。

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

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

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

      【讨论】:

        猜你喜欢
        • 2011-04-21
        • 2019-05-11
        • 1970-01-01
        • 2012-10-24
        • 2021-11-24
        • 2016-06-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多