编写线程安全类很困难。 它不仅需要仔细分析将在什么条件下读取或写入变量,而且还需要仔细分析该类如何被其他类使用。 有时,要在不损害类的功能,易用性或性能的情况下使其成为线程安全类是非常困难的。 一些类保留从一个方法调用到下一个方法调用的状态信息,并且很难以任何实际方式使此类成为线程安全的。
管理非线程安全类的使用可能比尝试使该类成为线程安全的要容易。 非线程安全的类通常可以在多线程程序中安全使用,只要您确保一个线程使用的该类的实例不被其他线程使用。 例如,JDBC Connection
类不是线程安全的-两个线程无法安全地共享精细级别的Connection
但是,如果每个线程都有自己的Connection
,那么多个线程可以安全地同时执行数据库操作。
当然可以在不使用ThreadLocal
情况下为每个线程维护一个单独的JDBC连接(或任何其他对象)。 Thread API为我们提供了将对象与线程关联所需的所有工具。 但是, ThreadLocal
类使我们更轻松地管理将线程与其单线程数据关联的过程。
什么是线程局部变量?
线程局部变量有效地为使用它的每个线程提供了其值的单独副本。 每个线程只能看到与该线程关联的值,并且不知道其他线程可能正在使用或修改其自己的副本。 一些编译器(例如Microsoft Visual C ++编译器或IBM XL FORTRAN编译器)已使用存储类修饰符(例如static
或volatile
)将对线程局部变量的支持合并到语言中。 Java编译器不提供对线程局部变量的特殊语言支持。 相反,它们是通过ThreadLocal
类实现的,该类在核心Thread
类中具有特殊的支持。
因为线程局部变量是通过类实现的,而不是作为Java语言本身的一部分来实现的,所以使用线程局部变量的语法比内置线程局部变量的语言方言更加笨拙。线程局部变量,您实例化ThreadLocal
类的对象。 ThreadLocal
类的行为与java.lang.ref
的各种Reference
类非常相似; 它充当存储或检索值的间接句柄。 清单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
作为系统中每个方法的参数都可能很不方便-更加草率,但更为方便的技术是使用Singleton访问连接。 但是,多个线程不能安全地共享JDBC Connection
。 通过在Singleton中使用ThreadLocal
(如清单3所示),我们可以允许程序中的任何类轻松获取对每个线程Connection
的引用。 这样,我们可以认为ThreadLocal
允许我们创建每个线程单线程 。
清单3.在每个线程的Singleton中存储JDBC连接
public class ConnectionDispenser {
private static class ThreadLocalConnection extends ThreadLocal {
public Object initialValue() {
return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
}
}
private static ThreadLocalConnection conn = new ThreadLocalConnection();
public static Connection getConnection() {
return (Connection) conn.get();
}
}
任何有状态或非线程安全的对象(比JDBC Connection
或正则表达式匹配器)创建起来都比使用起来昂贵的对象,更适合使用每线程单线程技术。 当然,对于这种情况,您可以使用其他方法(例如池化)来安全地管理共享访问。 但是,从可伸缩性的角度来看,甚至池化也存在一些潜在的缺点。 因为池实现必须同步才能维护池数据结构的完整性,所以如果所有线程都使用同一个池,则由于许多线程频繁访问该池的系统中的争用,程序性能可能会受到影响。
使用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
变量通过前面介绍的每线程单线程技术存储任何类型的每请求上下文信息。
ThreadLocal
的线程安全性较低的表亲InheritableThreadLocal
ThreadLocal
类具有一个相对的InheritableThreadLocal
,其功能相似,但是适用于完全不同的应用程序。 创建线程时,如果它包含任何InheritableThreadLocal
对象的值,那么这些值也将自动传递给子进程。 如果子进程在InheritableThreadLocal
上调用get()
,它将看到与父进程相同的对象。 为了保持线程安全,应仅将InheritableThreadLocal
用于不可变对象(一旦创建后其状态将永远不会更改的对象),因为该对象在多个线程之间共享。 InheritableThreadLocal
可用于将数据从父线程传递到子线程,例如用户ID或事务ID,但不适用于JDBC Connection
等有状态对象。
ThreadLocal
性能
尽管线程局部变量的概念已经存在了很长时间,并且得到包括Posix pthreads
规范在内的许多线程框架的支持,但最初的Java Threads设计中省略了线程局部支持,仅在Java版本1.2中添加了平台。 在许多方面, ThreadLocal
仍在开发中。 分别针对版本1.3和版本1.4进行了重写,以解决性能问题。
在JDK 1.2中,以类似于清单2的方式实现ThreadLocal
,除了使用同步的WeakHashMap
而不是HashMap
来存储值。 (使用WeakHashMap
解决了Thread
对象无法垃圾回收的问题,但会增加一些性能成本。)不用说, ThreadLocal
的性能相当差。
Java平台的1.3版随附的ThreadLocal
版本要好得多; 它不使用任何同步,因此不存在可伸缩性问题,并且也不使用弱引用。 取而代之的是,通过向Thread
中添加一个实例变量来修改Thread
类以支持ThreadLocal
,该实例变量包含一个HashMap
,该映射将线程局部变量映射到当前线程的值。 因为检索或设置线程局部变量的过程不涉及读取或写入可能由另一个线程读取或写入的数据,所以您可以实现ThreadLocal.get()
和set()
而不进行任何同步。 同样,由于对每个线程值的引用存储在拥有的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
中的对象,从而简化了确定类是否是线程安全的任务。
翻译自: https://www.ibm.com/developerworks/java/library/j-threads3/index.html