使用ThreadLocal变量的时机和方法

并发编程中,一个重要的内容是数据共享。当你创建了实现Runnable接口的线程,然后开启使用相同Runnable实例的各种Thread对象,所有 的线程便共享定义在Runnable对象中的属性。也就是说,当你在一个线程中改变任意属性时,所有的线程都会因此受到影响,同时会看到第一个线程修改后的值。有时我们希望如此,比如:多个线程增大或减小同一个计数器变量;但是,有时我们希望确保每个线程,只能工作在它自己的线程实例的拷贝上,同时不会影 响其他线程的数据。

使用ThreadLocal的时机

举个例子,想象你在开发一个电子商务应用,你需要为每一个控制器处理的顾客请求,生成一个唯一的事务ID,同时将其传到管理器或DAO的业务方法中,以便记录日志。一种方案是将事务ID作为一个参数,传到所有的业务方法中。但这并不是一个好的方案,它会使代码变得冗余。

你可以使用ThreadLocal类型的变量解决这个问题。首先在控制器或者任意一个预处理器拦截器中生成一个事务ID,然后在ThreadLocal中 设置事务ID,最后,不论这个控制器调用什么方法,都能从threadlocal中获取事务ID。而且这个应用的控制器可以同时处理多个请求,同时在框架 层面,因为每一个请求都是在一个单独的线程中处理的,所以事务ID对于每一个线程都是唯一的,而且可以从所有线程的执行路径获取。

扩展阅读:与JAX-RS ResteasyProviderFactory共享上下文数据(ThreadLocalStack实例)

ThreadLocal类

Java并发API为使用ThreadLocal类的局部线程变量提供了一个简洁高效的机制,

public class ThreadLocal<T> extends Object {...}

这个类提供了一个局部线程变量。这些变量不同于其所对应的常规变量,对于常规变量,每个线程只能访问(通过get或set方法)其自身所拥有的,独立初始化变量拷贝。在一个类中,ThreadLocal类型的实例是典型的私有、静态private static)字段,因为我们可以将其作为线程的关联状态(比如:用户ID或者事务ID)

这个类有以下方法:

  1. get():返回当前线程拷贝的局部线程变量的值。
  2. initialValue():返回当前线程赋予局部线程变量的初始值。
  3. remove():移除当前线程赋予局部线程变量的值。
  4. set(T value):为当前线程拷贝的局部线程变量设置一个特定的值。

怎样使用ThreadLocal?

下面的例子使用两个局部线程变量,即threadId和startDate。它们都遵循推荐的定义方法,即“private static”类型的字段。threadId用来区分当前正在运行的线程,startDate用来获取线程开启的时间。上面的信息将打印到控制台,以此验 证每一个线程管理他自己的变量拷贝。

class DemoTask implements Runnable {

   // Atomic integer containing the next thread ID to be assigned
   private static final AtomicInteger nextId = new AtomicInteger(0);

   // Thread local variable containing each thread's ID
   private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
               return nextId.getAndIncrement();
            }
         };

   // Returns the current thread's unique ID, assigning it if necessary
   public int getThreadId() {
      return threadId.get();
   }

   // Returns the current thread's starting timestamp
   private static final ThreadLocal<Date> startDate =
       new ThreadLocal<Date>() {
           protected Date initialValue() {
               return new Date();
           }
       };

   @Override
   public void run() {
      System.out.printf("Starting Thread: %s : %sn",
                        getThreadId(), startDate.get());
      try {
         TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 10));
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      System.out.printf("Thread Finished: %s : %sn",
                        getThreadId(), startDate.get());
   }
}

现在要验证变量本质上能够维持其自身状态,而与多线程的多次初始化无关。我们首先需要创建执行这个任务的三个线程,然后开启线程,接着验证它们打印到控制台中的信息。

Starting Thread: 0 : Wed Dec 24 15:04:40 IST 2014
Thread Finished: 0 : Wed Dec 24 15:04:40 IST 2014

Starting Thread: 1 : Wed Dec 24 15:04:42 IST 2014
Thread Finished: 1 : Wed Dec 24 15:04:42 IST 2014

Starting Thread: 2 : Wed Dec 24 15:04:44 IST 2014
Thread Finished: 2 : Wed Dec 24 15:04:44 IST 2014

在上面的输出中,打印出的声明序列每次都在变化。我已经把它们放到了序列中,这样对于每一个线程实例,我们都可以清楚地辨别出,局部线程变量保持着安全状态,而绝不会混淆。自己尝试下!

局部线程通常使用在这样的情况下,当你有一些对象并不满足线程安全,但是你想避免在使用synchronized关键字、块时产生的同步访问,那么,让每个线程拥有它自己的对象实例。

注意:局部变量是同步或局部线程的一个好的替代,它总是能够保证线程安全。唯一可能限制你这样做的是你的应用设计约束。

警告:在webapp服务器上,可能会保持一个线程池,那么ThreadLocal变量会在响应客户端之前被移除,因为当前线程可能被下一个请求重复使用。而 且,如果在使用完毕后不进行清理,它所保持的任何一个对类的引用—这个类会作为部署应用的一部分加载进来—将保留在永久堆栈中,永远不会被垃圾回收机制回收。

学习愉快!

原文链接: howtodoinjava 翻译: ImportNew.com - Angus
译文链接: http://www.importnew.com/14398.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: Angus

什么都不想说;新浪微博:@yannpzhao

查看Angus的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

1 条评论

  1. 二南 说道:

    In wabapp server, it may be keep a thread pool, so a ThreadLocal var should be removed before response to the client, since current thread may be reused by next request. Also, if you do not clean up when you’re done, any references it holds to classes loaded as part of a deployed webapp will remain in the permanent heap and will never get garbage collected.

    最后一句的翻译不太准确。

    Thumb up 0 Thumb down 0

跳到底部
返回顶部