什么时候以及如何使用 ThreadLocal 变量?

什么时候应该使用ThreadLocal变量?

如何使用?

答案

一种可能的(且常见的)用法是,当您有一些不是线程安全的对象,但又希望避免同步对该对象的访问时(我在看您, SimpleDateFormat )。而是给每个线程自己的对象实例。

例如:

public class Foo
{
    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

文件资料

由于ThreadLocal是对给定Thread数据的引用,因此在使用线程池的应用程序服务器中使用ThreadLocal时,可能会导致类加载泄漏。您需要非常小心,要使用ThreadLocalremove()方法清理get()set()的任何ThreadLocal

如果完成后不清理,则它对作为部署的 Webapp 的一部分加载的类的任何引用将保留在永久堆中,并且永远不会收集垃圾。重新部署 / 取消部署 Webapp 不会清除每个Thread对您 Webapp 的类的引用,因为Thread不是您的 Webapp 所拥有的。每次后续部署都将创建该类的新实例,该实例将永远不会被垃圾回收。

由于java.lang.OutOfMemoryError: PermGen space ,您最终将出现内存不足异常,并且在进行一些谷歌搜索之后,可能只会增加-XX:MaxPermSize而不是修复错误。

如果您确实遇到了这些问题,则可以使用Eclipse 的 Memory Analyzer和 / 或遵循Frank Kieviet 的指南后续步骤 ,确定哪个线程和类保留了这些引用。

更新:重新发现了Alex Vasseur 的博客条目 ,该条目帮助我追踪了我遇到的一些ThreadLocal问题。

许多框架使用 ThreadLocals 维护与当前线程相关的某些上下文。例如,当当前事务存储在 ThreadLocal 中时,您不需要通过每个方法调用将其作为参数传递,以防堆栈中有人需要访问它。 Web 应用程序可能将有关当前请求和会话的信息存储在 ThreadLocal 中,以便该应用程序可以轻松访问它们。使用 Guice,可以在为注入的对象实现自定义范围时使用 ThreadLocals(Guice 的默认servlet 范围很可能也使用它们)。

ThreadLocals 是一种全局变量(尽管邪恶程度稍低,因为它们仅限于一个线程),因此在使用它们时应小心避免不必要的副作用和内存泄漏。设计您的 API,以便在不再需要 ThreadLocal 值时将始终自动清除它们,并且将不可能错误使用 API(例如, 如下所示 )。 ThreadLocals 可以用来使代码更清洁,在某些罕见的情况下,他们是做什么工作的唯一方法(我当前的项目有两个这样的情况下,它们都记录在这里下的 “静态字段和全局变量”)。

在 Java 中,如果您的数据可能随每个线程而变化,则您的选择是将该数据传递给需要(或可能需要)该数据的每个方法,或将该数据与线程相关联。如果您的所有方法都已经需要传递一个公共的 “context” 变量,那么将数据传递到任何地方都是可行的。

如果不是这种情况,则可能不希望使用其他参数弄乱方法签名。在非线程世界中,可以使用等效于 Java 的全局变量来解决此问题。用线程词来说,全局变量的等效项是线程局部变量。

Java Concurrency in Practice》一书中有一个很好的例子。作者( Joshua Bloch )在这里解释了线程限制是实现线程安全的最简单方法之一,而ThreadLocal是维护线程限制的更正式的手段。最后,他还解释了人们如何通过将其用作全局变量来滥用它。

我已经从提到的书中复制了文本,但是缺少代码 3.10,因为了解在哪里使用 ThreadLocal 并不是很重要。

线程局部变量通常用于防止基于可变 Singleton 或全局变量的设计共享。例如,单线程应用程序可能会维护一个全局数据库连接,该连接在启动时会初始化,以避免必须将 Connection 传递给每个方法。由于 JDBC 连接可能不是线程安全的,因此使用全局连接而不进行额外协调的多线程应用程序也不是线程安全的。通过使用 ThreadLocal 存储 JDBC 连接,如清单 3.10 中的 ConnectionHolder 所示,每个线程将拥有自己的连接。

ThreadLocal 被广泛用于实现应用程序框架。例如,J2EE 容器在 EJB 调用期间将事务上下文与正在执行的线程相关联。这可以通过使用持有事务上下文的静态 Thread-Local 轻松实现:当框架代码需要确定当前正在运行的事务时,它将从此 ThreadLocal 获取事务上下文。这样做很方便,因为它减少了将执行上下文信息传递到每个方法中的需要,但是可以将使用此机制的任何代码耦合到框架。

通过将 ThreadLocal 的线程限制属性视为使用全局变量的许可或作为创建 “隐藏” 方法参数的方法,可以很容易地滥用 ThreadLocal。像全局变量一样,线程局部变量可能会损害可重用性,并在类之间引入隐藏的耦合,因此应谨慎使用。

本质上,当您需要一个变量的值来依赖当前线程时以其他方式 (例如,子类化线程) 将值附加到线程上并不方便

典型的情况是, 其他一些框架已经创建了代码在其中运行的线程 ,例如 servlet 容器,或者使用 ThreadLocal 更有意义,因为变量然后位于 “逻辑位置”(而不是变量)悬挂在 Thread 子类或其他哈希映射中)。

在我的网站上,我还进行了一些进一步的讨论,并提出了一些有关何时使用 ThreadLocal 的示例

有些人主张使用 ThreadLocal 作为在某些需要线程号的并发算法中将 “线程 ID” 附加到每个线程的方法(例如,参见 Herlihy&Shavit)。在这种情况下,请检查您是否真的受益匪浅!

  1. Java 的 ThreadLocal 在 JDK 1.2 上引入,但后来在 JDK 1.5 中被泛化,以在 ThreadLocal 变量上引入类型安全性。

  2. ThreadLocal 可以与 Thread 范围关联,由 Thread 执行的所有代码都可以访问 ThreadLocal 变量,但是两个线程彼此看不到 ThreadLocal 变量。

  3. 每个线程都拥有 ThreadLocal 变量的互斥副本,该副本在线程完成或死亡(正常情况下或由于任何异常)之后才有资格进行垃圾回收,因为这些 ThreadLocal 变量没有任何其他实时引用。

  4. Java 中的 ThreadLocal 变量通常是类中的私有静态字段,并在 Thread 中维护其状态。

阅读更多: Java 中的 ThreadLocal - 示例程序和教程

该文档说得很好:“每个(通过其 get 或 set 方法访问 [局部线程变量] 的线程)都有其自己的,独立初始化的变量副本”。

当每个线程必须具有自己的副本时,可以使用一个。默认情况下,线程之间共享数据。

Webapp 服务器可能会保留一个线程池,应在响应客户端之前删除ThreadLocal ,这样,下一个请求可以重用当前线程。

可以使用 threadlocal 变量的两个用例 -
1 - 当我们需要将状态与线程关联时(例如,用户 ID 或交易 ID)。对于 Web 应用程序,通常会发生这种情况,每个发送到 Servlet 的请求都具有唯一的与其关联的 transactionID。

// This class will provide a thread local variable which
// will provide a unique ID for each thread
class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        ThreadLocal.<Integer>withInitial(()-> {return nextId.getAndIncrement();});

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

请注意,此处的 withInitial 方法是使用 lambda 表达式实现的。
2 - 另一个用例是当我们想要一个线程安全的实例并且我们不想使用同步时,因为同步带来的性能成本更高。一种情况是使用 SimpleDateFormat。由于 SimpleDateFormat 不是线程安全的,因此我们必须提供使其成为线程安全的机制。

public class ThreadLocalDemo1 implements Runnable {
    // threadlocal variable is created
    private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue(){
            System.out.println("Initializing SimpleDateFormat for - " + Thread.currentThread().getName() );
            return new SimpleDateFormat("dd/MM/yyyy");
        }
    };

    public static void main(String[] args) {
        ThreadLocalDemo1 td = new ThreadLocalDemo1();
        // Two threads are created
        Thread t1 = new Thread(td, "Thread-1");
        Thread t2 = new Thread(td, "Thread-2");
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        System.out.println("Thread run execution started for " + Thread.currentThread().getName());
        System.out.println("Date formatter pattern is  " + dateFormat.get().toPattern());
        System.out.println("Formatted date is " + dateFormat.get().format(new Date()));
    } 

}