我应该何时以及如何使用线程局部变量?

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

它是如何使用的?

365186 次浏览

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

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

Java,如果你有一个每个线程都可以变化的数据,你的选择是将该数据传递给每个需要(或可能需要)它的方法,或者将数据与线程关联。如果你的所有方法都已经需要传递一个公共的“上下文”变量,那么到处传递数据可能是可行的。

如果不是这样,你可能不想用一个额外的参数弄乱你的方法签名。在一个非线程的世界里,你可以用一个全局变量的Java等价物来解决这个问题。在一个线程词中,全局变量的等价物是一个线程局部变量。

一种可能的(也是常见的)用法是,当你有一些对象不是线程安全的,但你想避免同步访问该对象(我在看着你,SimpleDateFormat简单日期格式)。相反,给每个线程自己的对象实例。

例如:

public class Foo{// SimpleDateFormat is not thread-safe, so give one to each threadprivate static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue(){return new SimpleDateFormat("yyyyMMdd HHmm");}};
public String formatIt(Date date){return formatter.get().format(date);}}

需求文档

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

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

由于java.lang.OutOfMemoryError: PermGen space,您最终会遇到内存溢出异常,经过一些谷歌搜索后,可能只会增加-XX:MaxPermSize,而不是修复bug。

如果您最终遇到这些问题,您可以通过使用Eclipse的内存分析器和/或遵循Frank Kieviet的指南后续行动来确定哪个线程和类保留这些引用。

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

本质上,当你需要一个变量的值取决于当前线程和它您不方便以其他方式将值附加到线程(例如,子类化线程)时。

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

在我的网站上,我还有一些可能感兴趣的讨论和示例何时使用ThreadLocal

有些人提倡使用ThreadLocal作为在某些需要线程号的并发算法中为每个线程附加“线程ID”的一种方式(参见例如Herlihy&Shavit)。在这种情况下,检查你是否真的得到了好处!

就像谷歌提到的,它的用法是定义一个全局变量,其中引用的值在每个线程中可以是唯一的。它的用法通常需要存储某种链接到当前执行线程的上下文信息。

我们在JavaEE环境中使用它来将用户身份传递给不JavaEE感知的类(无权访问HttpSession或EJB SessionContext)。这样,在基于安全的操作中使用身份的代码就可以从任何地方访问身份,而无需在每次方法调用中显式传递它。

大多数JavaEE调用中操作的请求/响应周期使这种类型的使用变得容易,因为它提供了定义良好的入口和出口点来设置和取消设置ThreadLocal。

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

ThreadLocals是一种全局变量(尽管由于它们仅限于一个线程而稍微不那么邪恶),因此在使用它们时应该小心,以避免不必要的副作用和内存泄漏。设计您的API,以便ThreadLocal值在不再需要时始终被自动清除,并且不可能错误使用API(例如像这样)。ThreadLocals可用于使代码更干净,在某些极少数情况下,它们是使某些事情正常工作的唯一方法(我当前的项目有两个这样的案例;它们记录在“静态字段和全局变量”下的这里)。

您必须非常小心ThreadLocal模式。有一些主要的缺点,如Phil提到的,但没有提到的是确保设置ThreadLocal上下文的代码不是“可重入的”。

当设置信息的代码第二次或第三次运行时,可能会发生糟糕的事情,因为您的线程上的信息可能会在您没有预料到的情况下开始发生变化。因此,在再次设置之前,请注意确保ThreadLocal信息尚未设置。

这里没有什么新东西,但是我今天发现ThreadLocal在Web应用程序中使用Bean验证时非常有用。验证消息是本地化的,但默认使用Locale.getDefault()。您可以将Validator配置为不同的MessageInterpolator,但是当您调用validate时无法指定Locale。因此,您可以创建一个静态ThreadLocal<Locale>(或者更好的是,一个包含其他您可能需要ThreadLocal的通用容器,然后让您的自定义MessageInterpolator从中选择Locale。下一步是编写一个Locale.getDefault()0,它使用会话值或Locale.getDefault()1来选择语言环境并将其存储在您的ThreadLocal引用中。

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

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

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

  3. 每个线程都持有ThreadLocal变量的独占副本,该变量在线程完成或死亡后(通常或由于任何异常)有资格进行垃圾回收,给定这些ThreadLocal变量没有任何其他活动引用。

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

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

缓存,有时您必须多次计算相同的值,因此通过存储方法的最后一组输入和结果,您可以加快代码速度。通过使用线程本地存储,您不必考虑锁定。

可以使用线程局部变量的两个用例-
1-当我们需要将状态与线程(例如,用户ID或事务ID)相关联时。这通常发生在Web应用程序中,每个发送到servlet的请求都有一个与之关联的唯一事务ID。

// This class will provide a thread local variable which// will provide a unique ID for each threadclass ThreadId {// Atomic integer containing the next thread ID to be assignedprivate static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's IDprivate static final ThreadLocal<Integer> threadId =ThreadLocal.<Integer>withInitial(()-> {return nextId.getAndIncrement();});
// Returns the current thread's unique ID, assigning it if necessarypublic static int get() {return threadId.get();}}

请注意,这里的方法with Inious是使用lambda表达式实现的。
2-另一个用例是当我们想要拥有一个线程安全的实例,而我们不想使用同步,因为同步的性能成本更高。一种这样的情况是使用SimpleDateFormat。由于SimpleDateFormat不是线程安全的,所以我们必须提供机制使其线程安全。

public class ThreadLocalDemo1 implements Runnable {// threadlocal variable is createdprivate static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>(){@Overrideprotected 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 createdThread t1 = new Thread(td, "Thread-1");Thread t2 = new Thread(td, "Thread-2");t1.start();t2.start();}
@Overridepublic 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()));}
}

Java实践中的并发中有一个非常好的例子。作者(Joshua Bloch)解释了线程限制是实现线程安全的最简单方法之一,线程本地是维护线程限制的更正式的方法。最后,他还解释了人们如何通过将其用作全局变量来滥用它。

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

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

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

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

ThreadLocal将确保多个变量访问可变对象非同步方法中的线程是同步的,意味着使方法中不可变的可变对象。

这是通过为每个线程提供可变对象的新实例来实现的尝试访问它。所以它是每个线程的本地副本。这是一些破解在要访问的方法中制作实例变量,如局部变量。如您所知,方法局部变量仅可用对于线程,一个区别是;方法局部变量不会一旦方法执行结束,线程就可以使用与线程本地共享的对象将跨多个可用直到我们清理它。

根据定义:

Java中的ThreadLocal类允许您创建可以只能由同一线程读取和写入。因此,即使两个线程正在执行相同的代码,并且该代码引用了ThreadLocal变量,则两个线程无法看到彼此的ThreadLocal变量。

java中的每个Thread都包含ThreadLocalMap
在哪里

Key = One ThreadLocal object shared across threads.value = Mutable object which has to be used synchronously, this will be instantiated for each thread.

实现线程本地:

现在为ThreadLocal创建一个包装器类,它将保存可变对象,如下所示(有或没有initialValue())。
现在,这个包装器的getter和setter将在线程本地实例而不是可变对象上工作。

如果线程本地的getter()在Thread;的threadlocalmap中没有找到任何值,那么它将调用初始化值()来获取它相对于线程的私有副本。

class SimpleDateFormatInstancePerThread {
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() {
@Overrideprotected SimpleDateFormat initialValue() {SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd") {UUID id = UUID.randomUUID();@Overridepublic String toString() {return id.toString();};};System.out.println("Creating SimpleDateFormat instance " + dateFormat +" for Thread : " + Thread.currentThread().getName());return dateFormat;}};
/** Every time there is a call for DateFormat, ThreadLocal will return calling* Thread's copy of SimpleDateFormat*/public static DateFormat getDateFormatter() {return dateFormatHolder.get();}
public static void cleanup() {dateFormatHolder.remove();}}

现在wrapper.getDateFormatter()将调用threadlocal.get(),这将检查currentThread.threadLocalMap包含这个(线程本地)实例。
如果是,则返回相应线程本地实例的值(SimpleDateFormat)
否则,添加带有此线程本地实例初始化值()的映射。

因此,在这个可变类上实现了线程安全;每个线程都使用自己的可变实例,但使用相同的ThreadLocal实例。意味着所有线程将共享相同的ThreadLocal实例作为键,但不同的SimpleDateFormat实例作为值。

https://github.com/skanagavelu/yt.tech/blob/master/src/ThreadLocalTest.java

什么时候?

当一个对象不是线程安全的,而不是阻碍可伸缩性的同步时,给每个线程一个对象并保持它的线程范围,即ThreadLocal。最常用但不是线程安全的对象之一是数据库连接和JMSConnection。

如何?

一个例子是Spring框架通过将这些连接对象保存在ThreadLocal变量中来大量使用ThreadLocal来管理幕后的事务。在高层,当事务启动时,它获取连接(并禁用自动提交)并将其保存在ThreadLocal中。在进一步的db调用中,它使用相同的连接与db通信。最后,它从ThreadLocal获取连接并提交(或回滚)事务并释放连接。

我认为log4j也使用ThreadLocal来维护MDC。

ThreadLocal是JVM专门提供的功能,仅为线程提供隔离的存储空间。就像实例作用域变量的值仅绑定到类的给定实例。每个对象都有其唯一的值,它们不能看到彼此的值。ThreadLocal变量的概念也是如此,它们在对象实例的意义上是线程本地的,除了创建它的线程之外,其他线程都看不到它。看这里

import java.util.concurrent.atomic.AtomicInteger;import java.util.stream.IntStream;

public class ThreadId {private static final AtomicInteger nextId = new AtomicInteger(1000);
// Thread local variable containing each thread's IDprivate static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());

// Returns the current thread's unique ID, assigning it if necessarypublic static int get() {return threadId.get();}
public static void main(String[] args) {
new Thread(() -> IntStream.range(1, 3).forEach(i -> {System.out.println(Thread.currentThread().getName() + " >> " + new ThreadId().get());})).start();
new Thread(() -> IntStream.range(1, 3).forEach(i -> {System.out.println(Thread.currentThread().getName() + " >> " + new ThreadId().get());})).start();
new Thread(() -> IntStream.range(1, 3).forEach(i -> {System.out.println(Thread.currentThread().getName() + " >> " + new ThreadId().get());})).start();
}}

当您想要拥有一些不应该在不同线程之间共享的状态,但应该在其整个生命周期内从每个线程访问时,ThreadLocal很有用。

举个例子,想象一个网络应用程序,其中每个请求都由不同的线程提供服务。想象一下,对于每个请求,你需要多次使用一段数据,这是相当昂贵的计算。然而,这些数据可能在每次传入请求时都发生了变化,这意味着你不能使用普通缓存。这个问题的一个简单快速的解决方案是有一个ThreadLocal变量来保存对这些数据的访问权限,这样你每个请求只需要计算一次。当然,这个问题也可以在不使用ThreadLocal的情况下解决,但我设计它是为了说明目的。

也就是说,请记住ThreadLocal本质上是全局状态的一种形式。因此,它有许多其他含义,只有在考虑了所有其他可能的解决方案后才应该使用。

线程局部变量通常用于防止基于可变单例或全局变量。

它可以在不使用连接池时为每个线程建立单独的JDBC连接等场景中使用。

private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {public Connection initialValue() {return DriverManager.getConnection(DB_URL);}};
public static Connection getConnection() {return connectionHolder.get();}

当您调用getConntions时,它将返回与该线程关联的连接。这同样可以对其他属性进行处理,例如日期格式、您不想在线程之间共享的事务上下文。

你也可以使用局部变量进行相同的操作,但是这些资源通常会占用创建时间,所以当你使用它们执行一些业务逻辑时,你不想一次又一次地创建它们。然而,ThreadLocal值存储在线程对象本身中,一旦线程被垃圾收集,这些值也会消失。

这个链接很好地解释了ThreadLocal的使用。

ThreadLocal提供了一种非常简单的方法来以零成本实现对象的可重用性。

我遇到过这样的情况:多个线程在每次更新通知上创建可变缓存的映像。

我在每个线程上使用了一个线程本地,然后每个线程只需要重置旧映像,然后在每次更新通知时从缓存中再次更新它。

来自对象池的通常可重用对象具有与它们相关的线程安全成本,而这种方法没有。

自Java8发布以来,有更多的声明式方法来初始化ThreadLocal

ThreadLocal<String> local = ThreadLocal.withInitial(() -> "init value");

在Java8发布之前,您必须执行以下操作:

ThreadLocal<String> local = new ThreadLocal<String>(){@Overrideprotected String initialValue() {return "init value";}};

此外,如果用于ThreadLocal的类的实例化方法(构造函数,工厂方法)不需要任何参数,您可以简单地使用方法引用(在Java8中介绍):

class NotThreadSafe {// no parameterspublic NotThreadSafe(){}}    
ThreadLocal<NotThreadSafe> container = ThreadLocal.withInitial(NotThreadSafe::new);

注:评估是惰性的,因为您传递的是java.util.function.Supplier lambda,仅在调用ThreadLocal#get但之前未评估值时才评估。

Java中的线程本地类使您能够创建只能由同一线程读写的变量。因此,即使两个线程正在执行相同的代码,并且代码引用了ThreadLocal变量,那么两个线程也无法看到彼此的ThreadLocal变量。

阅读更多

在多线程代码中使用类助手(如SimpleDateFormat)有3种情况,其中最好的一种是使用线程本地

场景

1-锁定或同步机制的帮助下使用共享对象,这使得应用程序缓慢

线程池场景

2-在方法中使用局部对象

在线程池中,在这种情况下,如果我们有4线程,每个都有1000任务次,那么我们有
4000SimpleDateFormat创建的对象并等待GC擦除它们

3-使用线程本地

在线程池中,如果我们有4个线程并且我们给了每个线程一个SimpleDateFormat实例
所以我们有4线程4个对象的SimpleDateFormat。

无需锁机制和对象的创建和销毁。(良好的时间复杂度和空间复杂度)

https://www.youtube.com/watch?v=sjMe9aecW_A

[供参考]ThreadLocal无法解决共享对象的更新问题。建议使用同一线程中所有操作共享的静态ThreadLocal对象。

方法必须由ThreadLocal变量实现,尤其是在使用线程池时,其中经常会复用线程,否则可能会影响后续的业务逻辑,造成内存泄漏等意外问题

试试这个小例子,了解ThreadLocal变量:

public class Book implements Runnable {private static final ThreadLocal<List<String>> WORDS = ThreadLocal.withInitial(ArrayList::new);
private final String bookName; // It is also the thread's nameprivate final List<String> words;

public Book(String bookName, List<String> words) {this.bookName = bookName;this.words = Collections.unmodifiableList(words);}
public void run() {WORDS.get().addAll(words);System.out.printf("Result %s: '%s'.%n", bookName, String.join(", ", WORDS.get()));}
public static void main(String[] args) {Thread t1 = new Thread(new Book("BookA", Arrays.asList("wordA1", "wordA2", "wordA3")));Thread t2 = new Thread(new Book("BookB", Arrays.asList("wordB1", "wordB2")));t1.start();t2.start();}}


控制台输出,如果先完成线程BookA:
结果BookA:'wordA1, wordA2, wordA3'。
结果BookB:'wordB1, wordB2'.

控制台输出,如果先完成线程BookB:
结果BookB:'wordB1, wordB2'.
结果BookA:'wordA1, wordA2, wordA3'。

第一个用例-每个线程上下文提供线程安全性和性能SpringFramework类中的实时示例-

  • 本地内容
  • 交易上下文持有人
  • 请求上下文
  • 日期时间上下文持有人

第二个用例-当我们不想在线程之间共享一些东西,同时由于性能成本不想使用同步/锁时示例-SimpleDateFormat为日期创建自定义格式

import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;
/*** @author - GreenLearner(https://www.youtube.com/c/greenlearner)*/public class ThreadLocalDemo1 {SimpleDateFormat sdf = new SimpleDateFormat("dd-mm-yyyy");//not thread safeThreadLocal<SimpleDateFormat> tdl1 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-dd-mm"));
public static void main(String[] args) {ThreadLocalDemo1 d1 = new ThreadLocalDemo1();
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i=0; i<100; i++) {es.submit(() -> System.out.println(d1.getDate(new Date())));}es.shutdown();}
String getDate(Date date){
//        String s = tsdf.get().format(date);String s1 = tdl1.get().format(date);return s1;}}

使用说明

  • 如果可能的话,使用局部变量。这样我们就可以避免使用ThreadLocal
  • 尽可能将功能委托给框架
  • 如果使用ThreadLocal并将状态设置为它,那么请确保在使用后清理它,否则它可能成为错误码的主要原因