`
yzgfbj
  • 浏览: 78494 次
  • 来自: ...
社区版块
存档分类
最新评论

轻松使用线程: ThreadLocal

阅读更多
利用 ThreadLocal 提高可伸缩性

ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。由于这些原因,ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。在轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。
参加 Brian 的多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。

编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它类能如何使用某个类。有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是困难的。

管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类是非线程安全的 — 两个线程不能在小粒度级上安全地共享一个 Connection — 但如果每个线程都有它自己的 Connection,那么多个线程就可以同时安全地进行数据库操作。

不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的;Thread API 给了我们把对象和线程联系起来所需的所有工具。而 ThreadLocal 则使我们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。

什么是线程局部变量(thread-local variable)?
线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 static 或 volatile)把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语言支持;相反地,它用 ThreadLocal 类实现这些支持,核心 Thread 类中有这个类的特别支持。

因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似;ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了 ThreadLocal 接口。

清单 1. ThreadLocal 接口
public class ThreadLocal {
  public Object get();
  public void set(Object newValue);
  public Object initialValue();
}


get() 访问器检索变量的当前线程的值;set() 访问器修改当前线程的值。initialValue() 方法是可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它清楚地说明了 ThreadLocal 的工作方式。

清单 2. ThreadLocal 的糟糕实现

public class ThreadLocal {
  private Map values = Collections.synchronizedMap(new HashMap());

  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }

  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }

  public Object initialValue() {
    return null;
  }
}



这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且如果多个线程同时访问同一个 ThreadLocal,那么将发生争用。此外,这个实现也是不切实际的,因为用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。

用 ThreadLocal 实现每线程 Singleton
线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进 ThreadLocal。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 — 用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。如清单 3 所示,通过使用“单子”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建每线程单子。

清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中
public class ConnectionDispenser {
  private static class ThreadLocalConnection extends ThreadLocal {
    public Object initialValue() {
      return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
    }
  }

  private ThreadLocalConnection conn = new ThreadLocalConnection();

  public static Connection getConnection() {
    return (Connection) conn.get();
  }
}




任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,您可以使用其它技术,例如用池,来安全地管理共享访问。然而,从可伸缩性角度看,即使是用池也存在一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那么在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。

用 ThreadLocal 简化调试日志纪录
其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的工具。您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。在一个工作单元的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调试信息。

清单 4. 用 ThreadLocal 管理每线程调试日志
public class DebugLogger {
  private static class ThreadLocalList extends ThreadLocal {
    public Object initialValue() {
      return new ArrayList();
    }

    public List getList() {
      return (List) super.get();
    }
  }

  private ThreadLocalList list = new ThreadLocalList();
  private static String[] stringArray = new String[0];

  public void clear() {
    list.getList().clear();
  }

  public void put(String text) {
    list.getList().add(text);
  }

  public String[] get() {
    return list.getList().toArray(stringArray);
  }
}





在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对象)相比,这种技术简便得多,也有效得多。

ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。

ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal
ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完全不同的应用程序。创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get(),那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会被改变的对象)使用 InheritableThreadLocal,因为对象被多个线程共享。InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection。

ThreadLocal 的性能
虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面,ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是为了性能问题。

在 JDK 1.2 中,ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。)不用说,ThreadLocal 的性能是相当差的。

Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保存当前线程的从线程局部变量到它的值的映射的 HashMap)来修改 Thread 类以支持 ThreadLocal。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set()。而且,因为每线程值的引用被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程值进行垃圾回收。

不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。据我的粗略测量,在双处理器 Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被频繁地执行,或者同步块很大),ThreadLocal 可能仍然要高效得多。

在 Java 平台的最新版本,即版本 1.4b2 中,ThreadLocal 和 Thread.currentThread() 的性能都有了很大提高。有了这些提高,ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。

ThreadLocal 的好处
ThreadLocal 能带来很多好处。它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 — 通过使用 ThreadLocal,存储在 ThreadLocal 中的对象都是不被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工作。

我希望您从这个系列中得到了乐趣,也学到了知识,我也鼓励您到我的讨论论坛中来深入研究多线程问题。



关于作者
Brian Goetz 是一名软件顾问,过去 15 年来一直是专业软件开发者。他是 Quiotix 的首席顾问,该公司从事软件开发和咨询业务,位于加利福尼亚的 Los Altos。敬请查看 Brian 在流行的业界出版物中已发表和即将发表的论文列表。可通过 brian@quiotix.com 与 Brian 联系。
分享到:
评论

相关推荐

    C# 线程使用总结

    在C#中,可以通过两种主要方式创建线程:使用`Thread`类或使用`Task`类。`Thread`类是最原始的方法,直接实例化`Thread`对象并调用`Start()`方法启动新线程。而`Task`类是.NET Framework 4.0引入的,用于异步编程,...

    轻松理解多线程

    11. **线程局部存储**:使用 `ThreadLocal<T>` 类,可以在每个线程中维护独立的数据副本,避免了线程同步的复杂性。 回到我们的搬家公司的例子,最初的单线程模型(老板一个人做所有事情)限制了公司的发展,当业务...

    Java ThreadLocal用法实例详解

    Java ThreadLocal用法实例详解 Java ThreadLocal是Java中的一种线程局部变量机制,用于保存每个线程独有的数据,以避免线程...通过使用ThreadLocal,开发者可以轻松地实现线程局部变量,避免线程之间的数据共享问题。

    VB 多线程 多个参数传入

    9. **线程局部存储**: 对于线程相关的数据,可以使用`ThreadLocal`类,保证每个线程有自己的数据副本,避免了同步开销。 通过以上介绍,你可以开始在VB中创建和管理多线程程序,并合理传递和处理多个参数。记住,...

    Hystrix跨线程传递数据解决方案:HystrixRequestContext.docx

    - **检查初始化状态**:使用 `HystrixRequestContext.isCurrentThreadInitialized()` 方法检查当前线程是否已经初始化了 `HystrixRequestContext`。 ##### HystrixRequestVariable 与 ...

    VS2010轻松学习C#-从零到深入-天轰穿.NET4趣味编程视频教程_第43讲:线程通信与异步委托

    4. **线程局部存储(Thread Local Storage, TLS)**:使用`System.Threading.ThreadLocal<T>`类,每个线程都可以拥有自己的独立数据副本,避免了线程同步的复杂性。 5. **线程池**:C#的`ThreadPool`类可以管理一组...

    线程相关内容

    C#中的`System.Threading`命名空间包含了大量与线程操作相关的类和方法,使得开发者能够轻松地创建、管理和控制线程。 1. **线程创建**:在C#中,可以通过`Thread`类创建新的线程。创建一个新的线程对象,并通过...

    浅谈.Net下的多线程和并行计算(全集)

    在.NET中,可以使用`System.Threading`命名空间中的`Thread`类来创建和管理线程。创建新线程的基本步骤包括:定义线程的入口方法,实例化`Thread`对象,并调用`Start`方法启动线程。此外,还有线程池(ThreadPool)...

    swift-ThreadlySwift中类型安全的本地线程存储

    3. **简单易用的API**:ThreadlySwift提供了一组易于理解和使用的API,使得开发者可以轻松地在每个线程中创建、读取和删除存储的数据。例如,可以使用`ThreadLocal<T>`结构体来定义一个线程局部变量,其中`T`是存储...

    C#多线程机制探索与揭密

    在C#中,可以使用`ThreadLocal<T>`类来创建线程局部变量。 六、线程优先级 C#的`Thread`类提供了设置线程优先级的选项,如`Priority.High`、`Priority.Normal`和`Priority.Low`。然而,操作系统对优先级的调度策略...

    苍穹外卖-共享当前登录用户ID.docx

    由于`ThreadLocal`中存储的数据仅对当前线程可见,因此在Controller层、Service层或Mapper层等需要使用用户ID的地方,只需调用`BaseContext.getCurrentId()`即可轻松获取。 4. **清理工作** 为了避免内存泄漏,...

    2022java面试题、JVM面试题、多线程面试题、并发编程、

    多线程和并发编程是Java应用中常见的技术,涉及到线程的创建、同步、死锁等问题,以及Java并发API的使用,如synchronized、volatile、ThreadLocal、ConcurrentHashMap等。深入理解这些知识点,将有助于开发者在实际...

    ASP.NET中的实用多线程

    `ThreadLocal<T>`类允许你在每个线程中维护独立的实例,这对于需要线程局部数据的场景非常有用。例如,你可以创建一个`ThreadLocal`变量来存储线程特有的日志记录器: ```csharp private static readonly ...

    flexmojos-threadlocaltoolkit-wrapper-6.0.1.zip

    了解这些知识点后,开发者可以利用 FlexMojos 进行 Flex 应用的构建,通过 ThreadLocalToolkit 来高效地管理多线程环境中的变量,而 Play-2-Mail 插件则可以帮助他们在 Play 应用中轻松实现邮件功能。

    concurrent 源代码

    7. **线程局部变量**: `ThreadLocal`类提供了一种方法,每个线程都可以拥有独立的变量副本,避免了多线程间的共享状态问题。 8. **线程池**: `ThreadPoolExecutor`是`ExecutorService`的一个实现,允许开发者自定义...

    Android Studio —— Thread

    它可以轻松地在后台线程中执行操作,并在完成后更新UI。然而,由于内存泄漏和生命周期管理问题,建议在Android P及更高版本中避免使用AsyncTask。 2. **Handler/Looper/Message** 这是一种基于消息传递的机制,...

    slf4j中的MDC

    - 添加信息:使用`MDC.put(key, value)`方法将诊断信息添加到MDC,key是唯一标识,value是对应的信息。 - 获取信息:使用`MDC.get(key)`可以获取之前添加的信息。 - 清除信息:`MDC.clear()`方法可以清除所有MDC...

    C#中的生成随机数代码

    在处理并发或多线程场景时,为了防止多个线程使用相同的随机数种子,可以考虑使用`ThreadLocal<Random>`来确保每个线程有自己的`Random`实例: ```csharp ThreadLocal<Random> randomThreadLocal = new ThreadLocal...

    ConcurencyITILabs

    6. **线程局部变量**:ThreadLocal类提供了一种线程独享的变量,每个线程都有自己的副本,互不影响,常用于存储线程上下文信息。 7. **死锁、活锁与饥饿**:并发编程中可能遇到的三种问题。死锁是两个或更多线程...

Global site tag (gtag.js) - Google Analytics