什么是线程本地存储的用途和必要性?

7

我正在研究Java中的ThreadLocal。我无法理解为什么我们需要这个类。如果我只是简单地为每个线程传递一个新对象来执行,我就可以实现相同的目的,因为使用initialValue()时会发生同样的事情。我只需在initialvalue()中为每个线程返回一个新对象。

但是假设我有两个线程,ThreadOne:A和ThreadTwo:B。现在我想让它们拥有自己的SimpleDateFormat类的副本。我可以通过将SimpleDateFormat对象封装在ThreadLocal类中,然后使用initialValue()返回new SimpleDateFormat("yyyyMMdd HHmm");来实现这一点。我也可以创建两个新的SimpleDateFormat对象,并将其分别传递给ThreadOne:A和ThreadTwo:B。ThreadLocal如何帮助我额外地实现这一点呢?

谢谢,


1
这里有一些不错的答案:https://dev59.com/k3RA5IYBdhLWcg3wzhXZ - JSS
2
在我写多线程应用程序的30年中,我从未见过有关线程本地变量的令人信服的论据,也从未感到需要使用它们。如果我需要一个“线程本地”的东西,那么它将作为线程类的成员输入 - 所有成员函数都可以访问它,每个线程一个“something”,完成工作。 - Martin James
谢谢你的点赞 :)。但我仍然觉得需要更多地探讨这个主题,才能得到一个满意的答案。 - nits.kk
4个回答

6

有一些不错的例子在这里与您的问题相关。

但我试图解释第二部分:

但是,假设我有两个线程,ThreadOne:A 和 ThreadTwo:B。现在我想让它们都拥有自己的 SimpleDateFormat 类的副本。我可以通过将 SimpleDateFormat 对象包装在 ThreadLocal 类中来实现这一点,然后使用 initialValue() 返回新的 SimpleDateFormat("yyyyMMdd HHmm");。如果我创建两个 SimpleDateFormat 的新对象并将其中一个传递给 ThreadOne:A,另一个传递给 ThreadTwo:B,我也可以实现相同的目的。ThreadLocal 如何帮助我呢?

通常,您需要使用特定的格式来格式化日期,并且当然最好仅创建一次 SimpleDateFormat 对象(而不是为每次需要格式化日期时都创建一个新的 SimpleDateFormat)。

因此,您可能会像这样写:

public class DateUtils {  
    private final static DateFormat dateFormat = new SimpleDateFormat("dd-mm-yyyy");  

    public String formatDate(Date date) {  
        return dateFormat.format(date);  
    }  
}  

如果多个线程同时调用formatDate(...),这将失败(您可能会得到奇怪的输出或异常),因为SimpleDateFormat不是线程安全的。为了使其线程安全,您可以使用ThreadLocal

public class DateUtils {  
    private final ThreadLocal<DateFormat> dateFormat = new ThreadLocal<DateFormat>() {  
        @Override  
        protected DateFormat initialValue() {  
            return new SimpleDateFormat("dd-mm-yyyy");  
        }  
    };  

    public String formatDate(Date date) {  
        return dateFormat.get().format(date);  
    }  
}  

现在每个线程(或调用)formatDate()方法都会在本地副本上工作,不会互相干扰。这使得它具备了线程安全的行为。

谢谢您的回复。但是还有一件事情我不太清楚。<p> public String formatDate(Date date) {
return new SimpleDateFormat("dd-mm-yyyy").format(date);
} 这样做也会产生相同的效果。从initialValue()返回new SimpleDateFormat("dd-mm-yyyy");会为每个线程创建新对象。那么在这种情况下,ThreadLocal有什么用处呢?它的正确使用方法是什么?
- nits.kk
@nits.kk 你找到令人满意的解释了吗?我仍然不知道在类中保留“private ThreadLocal<T>”和“private static Map<ThreadId, T>”之间的区别将是什么。 - rents

1

线程本地存储在单个线程的上下文中用于全局变量的目的。

考虑以下示例:您编写了一个多线程程序来处理用户请求。多个用户可以同时发起请求;您的系统为每个用户使用一个线程。

当收到用户请求时,您的系统确定其来自哪个用户,并为该用户创建一个UserPermissions对象实例。

有几种方法可以使该对象在正在运行的程序中可用。一种方法是将UserPermissions传递给可能需要它的每个方法,以及直接或间接调用可能需要它的方法。在使用回调的情况下,这可能会有问题。

如果您的程序不是多线程的,则可以将UserPermissions设置为全局变量。不幸的是,您不能这样做,因为多个用户请求可能同时处于活动状态。

这就是线程本地存储的作用:创建用户权限的进程将UserPermissions对象设置为线程本地存储,并将其保留在那里,直到请求的处理结束。这样,所有方法都可以根据需要获取UserPermissions,而无需将其作为方法参数传递。


作为一个线程类的数据成员/字段,UserPermissions有什么问题?这样一来,所有线程类的方法都可以根据需要获取UserPermissions,而无需将它们作为方法参数传递。 - Martin James
@MartinJames 这种方法的一个大问题是需要为线程创建子类。访问用户信息的需求“横跨”多个类,其中一些可能与线程实现类无关。 - Sergey Kalinichenko

0
你可以使用 ThreadLocal 将 "数据" 传递给一个 特定的 线程。
例如你有一个方法 doSomething(SomeObject a, SomeOtherObject b);
通过线程局部变量(ThreadLocal),你可以以线程安全的方式为执行线程传递更多信息,而不仅仅是 ab

没有帮助。将新数据加载到线程本地变量中与将新数据加载到线程类实例的数据字段中一样,即通常情况下不是线程安全的,需要同步。 - Martin James
@MartinJames:不,这是错误的。ThreadLocal 是唯一的,并且只能被特定线程访问。不需要同步。 - Cratylus
什么数据?您能提供一个具体的例子吗? - Cratylus

0

线程本地变量是实现动态作用域的一种廉价方式。在动态作用域中,绑定存在于代码块的评估期间。在这种情况下,绑定存在于特定线程的执行期间。早期的Lisp支持动态作用域,但它很少有意义,因此大多数现代编程语言不支持它,除非通过线程本地变量。

动态作用域/线程本地变量对于维护普遍上下文信息非常有用,例如:

有一条名为“面向上下文编程”的研究线路旨在更好地支持编程语言中的此类问题。文章“{{link4:面向上下文编程:超越层次结构} }”展示了一些进一步的例子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接