线程同步是进行多线程编程时所必须考虑的一个问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。如果能为每个线程提供一份需要共享的数据的copy,那么对该数据的访问也就没有必要进行同步了。Thread Local Storage就是能够达到这个目的的一个多线程设计模式。Thread Local Storage,顾名思义,就是“线程本地数据”,指每个线程拥有各自独立的数据拷贝。Thread Local Storage还有另外一些称呼:Thread Specific Storage,Thread Specific Data等。
Java类库中的ThreadLocal类就是该模式的一个实现。在该类的api文档中有如此说明:ThreadLocal类型的变量不同于普通变量,每个访问它的线程都有一份各自独立初始化的copy,对它的访问是通过get/set方法实现的。ThreadLocal实例典型情况下是类的private static字段。
下面一段程序使用了ThreadLocal类。这个程序假定用于对网站的访问日志进行某种处理,如果有N个日志文件需要处理,就启动N个线程,需要记录处理每个日志文件所花费的时间。
01public class LogStats {
02private static final ThreadLocal logFile =newThreadLocal();
03private static final ThreadLocal startTime =newThreadLocal();
04
05public static void init(String logFileName) {
06logFile.set(logFileName);
07startTime.set(System.currentTimeMillis());
08}
09
10public static void process() {
11open(logFile.get());
12readAndProcessEachRecord();
13long time = System.currentTimeMillis() - startTime .get();
14System.out.println(logFile.get() +" processed: " + time +"ms.");
15}
16
17public static void main(String[] args) {
18for (final String logFileName : args) {
19new Thread() {
20public void run() {
21LogStats.init(logFileName);
22LogStats.process();
23}
24}.start();
25}
26}
27}
为简明起见,省略了部分代码。执行这个程序,可能会产生如下输出结果:
1java LogStats access.log.1 access.log.2 access.log.3
2access.log.1 processed: 1294538ms
3access.log.2 processed: 1265327ms
4access.log.3 processed: 1189563ms
在get/set方法被调用时,ThreadLocal能根据执行的上下文(即Thread.currentThread())确定变量的值。
从外观上看,ThreadLocal类似一个HashMap,其键相当于Thread.currentThread(),值为线程的变量值。据此实现一个自定义版本的ThreadLocal类型是比较直接的:
01public class CustomizedThreadLocal {
02private Map map = Collections.synchronizedMap(newHashMap());
03protected T initialValue () {
04return null;
05}
06
07public T get() {
08if (!map.containsKey(Thread.currentThread())) {
09map.put(Thread.currentThread(), initialValue());
10}
11return map.get(Thread.currentThread());
12}
13
14public void set(T value) {
15map.put(Thread.currentThread(), value);
16}
17
18public void remove() {
19map.remove(Thread.currentThread());
20}
21}
可以使用CustomizedThreadLocal来代替LogStats中的ThreadLocal的功能,当然效率上可能会有所差别。
在LogStats代码中,对logFile和startTime的访问没有进行显式的同步。但这并不意味着使用Thread Local Storage模式时,可以完全消除同步,只不过是在应用层代码中可以不进行同步。在ThreadLocal的内部,线程同步可能发生在对各线程变量值的访问时,就像在CustomizedThreadLocal中所做的一样(通过sychronizedMap达到同步)。查看Java类库的ThreadLocal的源码发现,对ThreadLocal的访问是这样的:JVM为每个Thread维护一个该线程独有变量的Map,称为ThreadLocalMap,这是通过本地代码(native方法)来实现的,当访问ThreadLocal时,根据ThreadLocal实例的Hash值在当前线程的ThreadLocalMap中查找对应的变量值,而一个线程对其自身的ThreadLocalMap的访问必然是单线程的而无须同步。不过在本地代码中,JVM底层可能会施加某些同步措施。
LogStats的代码只是为了演示ThreadLocal的使用,并不是良好的OO设计风格。这里使用ThreadLocal仅只是记录执行时间,没有什么实际意义,似乎完全没有必要。但是考虑在一个更大的应用程序范围内,线程的执行可能会跨越很深的代码层次结构,比如在一个基于servlet的web应用中处理用户请求的线程,要使某些变量在线程执行期间对相距甚远的两部分代码可见,除了在每个方法调用处将变量作为参数传递(这显然是一种很令人头疼的做法),另一个选择就是使用ThreadLocal静态变量。
ThreadLocal变量是和线程绑定的,而不是和任务绑定。忽视这点,可能会造成一些意想不到的问题。在线程池模式中,一个线程会被多次用来执行不同的任务,比如在servlet中,是采用线程池来为用户请求服务(这里的任务是用户请求):当一个http请求到达时,servlet容器会从线程池中取出一个空闲线程处理请求,处理完毕,再将线程放回线程池,以便可以继续为后续的请求服务。我曾在一次开发过程中,遇到因ThreadLocal使用不当造成的问题。
具体情况是这样的:当用户访问我们的网站服务(服务端是基于servlet)时,需要针对来自某种类型的用户终端的请求进行特殊处理,给出不同的响应。在服务端代码中,很多不同的模块都要对该终端类型进行判断,因此,我使用Filter,在处理请求之前,识别出该终端类型并保存在ThreadLocal静态变量中,以便其它模块直接使用。代码大体如下:
01public class UserAgentFilterimplements Filter {
02public static final ThreadLocal specialUA =newThreadLocal() {
03protected Boolean initialValue() {
04return false;
05}
06};
07public void doFilter(ServletRequest request, ServletResponse response) {
08if (isSpecialUserAgent(request)) {//1//
09specialUA.set(true);
10}
11
12}
13}
这段功能上线后,通过对访问日志的分析发现,对一部分原本不是该类型的终端请求也进行了特殊处理。造成这种现象的原因是:第一次,当一个来自特殊终端的请求到达时,在1处条件判断为真,从而将specialUA的值设置为true,当请求处理结束时,处理该请求的线程被放入线程池。第二次,当一个普通终端的请求到达时,线程池中的一个空闲线程被选中来为该请求服务,选中的线程碰巧是第一次的请求处理线程,而此时在1处条件判断为假,specialUA的值在此次请求中未被设置,但却仍然为true。因为ThreadLocal变量的值是该线程在处理第一次请求时被设置的。
对该问题的解决方法是:在请求处理结束时,线程被放入线程池之前,将specialUA的值设置为默认值(false),或者在每次请求开始时都重新设置该值:
1......
2if (isSpecialUserAgent(request)) {//1//
3specialUA.set(true);
4}else {
5specialUA.set(false);
6}
7......
Thread Local Storage是一种很有用的多线程设计模式,特别是在将单线程程序改为多线程时。Java中的ThreadLocal类是该模式的一个实现。ThreadLocal变量值是和线程绑定的,不是和任务绑定。当ThreadLocal(或Thread Local Storage模式)和线程池模式一起使用时,一定要在任务开始或结束之时,将ThreadLocal变量设置为默认值,以防出现不可预料的问题。