正如我们的读者可能已经猜到的那样,我每天都在处理内存泄漏。 最近,一种特殊类型的OutOfMemoryError消息开始引起我的注意-滥用ThreadLocals触发的问题变得越来越频繁。 在查看此类泄漏的原因时,我开始相信其中一半以上是由于开发人员导致的,他们要么不知道自己在做什么,要么正试图对并非旨在解决的问题采用解决方案。 。
我决定不发表文章,而是发表两篇文章来打开这个话题,您目前正在阅读第一篇。 在帖子中,我解释了ThreadLocal使用背后的动机。 在当前正在进行的第二篇文章中,我将打开ThreadLocal帽子并查看实现。
让我们从一个虚构的场景开始,其中ThreadLocal的使用确实是合理的。 为此,请与我们的假设开发人员Tim打个招呼。 蒂姆正在开发一个Web应用程序,其中包含许多本地化内容。 例如,来自加利福尼亚的用户可能希望使用熟悉的MM / dd / yy模式格式化日期,而来自爱沙尼亚的用户则希望看到根据dd.MM.yyyy格式化的日期。 因此,Tim开始编写如下代码:
public String formatCurrentDate() {
DateFormat df = new SimpleDateFormat("MM/dd/yy");
return df.format(new Date());
}
public String formatFirstOfJanyary1970() {
DateFormat df = new SimpleDateFormat("MM/dd/yy");
return df.format(new Date(0));
}
过了一会儿,Tim觉得这很无聊并且违反了良好做法–应用程序代码被此类初始化污染了。 因此,他通过将DateFormat提取到实例变量中采取了看似合理的措施。 采取行动之后,他的代码现在如下所示:
private DateFormat df = new SimpleDateFormat("MM/dd/yy");
public String formatCurrentDate() {
return df.format(new Date());
}
public String formatFirstOfJanyary1970() {
return df.format(new Date(0));
}
Tim对重构结果感到满意,Tim向自己扔了一个假想的高五,将更改推送到存储库中,然后回家。 几天后,用户开始抱怨–其中一些字符串似乎完全乱码,而不是以前格式正确的日期。
研究问题Tim发现DateFormat实现不是线程安全的。 这意味着在上述情况下,如果两个线程同时使用formatCurrentDate()和formatFirstOfJanyary1970()方法,则状态可能会混乱,并且显示的结果可能会混乱。 因此,Tim通过限制对方法的访问以确保一次输入一个线程进入格式化功能来解决此问题。 现在,他的代码如下所示:
private DateFormat df = new SimpleDateFormat("MM/dd/yy");
public synchronized String formatCurrentDate() {
return df.format(new Date());
}
public synchronized String formatFirstOfJanyary1970() {
return df.format(new Date(0));
}
在给自己另一个虚拟的高五后,蒂姆做出了改变并去了一个漫长的假期。 第二天才开始接收电话,抱怨应用程序的吞吐量急剧下降。 深入研究问题后,他发现同步访问已在应用程序中造成了意外的瓶颈。 现在,线程不必再随意输入格式化部分了,而必须彼此等待。
进一步阅读该问题,Tim发现了另一种类型的变量ThreadLocal 。 这些变量与普通变量不同,因为每个访问一个线程(通过ThreadLocal的get或set方法)的线程都有其自己的,独立初始化的变量副本。 对于新发现的概念感到满意,Tim再次重写了代码:
public static ThreadLocal df = new ThreadLocal() {
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yy");
}
};
public String formatCurrentDate() {
return df.get().format(new Date());
}
public String formatFirstOfJanyary1970() {
return df.get().format(new Date(0));
}
经过这样的过程,蒂姆通过痛苦的教训学到了一个强大的概念。 像在最后一个示例中那样应用后,结果可以很好地说明收益。
但是新发现的概念很危险。 如果Tim使用了应用程序类之一而不是引导类加载器加载的JDK捆绑的DateFormat类,那么我们已经处在危险区域。 只是忘记在手头的任务完成后将其删除,该对象的副本将保留在线程中,该线程通常属于线程池。 由于池化线程的寿命超过了应用程序的寿命,因此它将防止该对象,从而使ClassLoader负责加载应用程序而不会被垃圾回收。 而且我们创建了一个泄漏,有机会以一种很好的旧java.lang.OutOfMemoryError:PermGen空间形式浮出水面。
另一种开始滥用该概念的方法是通过使用ThreadLocal作为在应用程序中获取全局上下文的黑客。 克服这个难题是确保应用程序代码具有各种无法想象的依赖关系的可靠方式,将整个代码库耦合到一个无法维护的混乱中。
翻译自: https://www.javacodegeeks.com/2013/10/when-and-how-to-use-a-threadlocal.html