ThreadLocal
类是悄悄地出如今 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads
工具)的一部分,但 Java Threads API 的最初设计却没有这项有用的功能。并且,最初的实现也至关低效。因为这些缘由, ThreadLocal
极少受到关注,但对简化线程安全并发程序的开发来讲,它倒是很方便的。在 轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal
并提供了一些使用技巧。
编写线程安全类是困难的。它不但要求仔细分析在什么条件能够对变量进行读写,并且要求仔细分析其它类能如何使用某个类。 有时,要在不影响类的功能、易用性或性能的状况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是困难的。
管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类一般能够安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection
类是非线程安全的 — 两个线程不能在小粒度级上安全地共享一个 Connection
— 但若是每一个线程都有它本身的 Connection
,那么多个线程就能够同时安全地进行数据库操做。
不使用 ThreadLocal
为每一个线程维护一个单独的 JDBC 链接(或任何其它对象)固然是可能的;Thread API 给了咱们把对象和线程联系起来所需的全部工具。而 ThreadLocal 则使咱们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。
线程局部变量高效地为每一个使用它的线程提供单独的线程局部变量值的副本。每一个线程只能看到与本身相联系的值,而不知作别的线程可能正在使用或修改它们本身的副本。一些编译器(例如 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
的特定于线程的值进行垃圾回收。
线程局部变量常被用来描绘有状态“单子”(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
但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想建立一个用于管理多线程应用程序调试信息的工具。您能够用如清单 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,它以类似的方式工做,但适用于种类彻底不一样的应用程序。建立一个线程时若是保存了全部 InheritableThreadLocal
对象的值,那么这些值也将自动传递给子线程。若是一个子线程调用 InheritableThreadLocal
的 get()
,那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦建立,其状态就永远不会被改变的对象)使用 InheritableThreadLocal
,由于对象被多个线程共享。 InheritableThreadLocal
很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection
。
虽然线程局部变量早已赫赫有名并被包括 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
中的对象都是 不被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工做。