最早接触ThreadLocal这个东东,还是在学Hibernate的时候,当时看ThreadLocal没明白是干什么的,后来在网上查才明白ThreadLocal的用途,ThreadLocal其实蛮有用的,总结一下具体的原理及用法。

虽然支持线程局部变量早就是许多线程工具,但 Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。

ThreadLocal要解决的是什么问题呢?

一个本来应该线程安全的类,里面有一个线程不安全的变量,这样这个类也就线程不安全了,那应该怎么办呢?我们如果能够把这个变量和每个线程绑定,也就是每一个线程拥有这个变量的副本,那么整个对象就成为线程安全的了。一个解决方案就是使用一个Map,key对应于当前的线程,value对应于那个变量,这样我们就可以轻易的获取到当前线程的那个变量的副本了,ThreadLocal就是这个东东。

我们不妨写写大致的代码:

Java代码


  1. public class ThreadLocal{
  2. private Map values = Collections.synchronizedMap(new HashMap());
  3. public Object get(){
  4. Thread curThread = Thread.currentThread();
  5. Object o = values.get(curThread);
  6. if (o == null && !values.containsKey(curThread)){
  7. o = initialValue();
  8. values.put(curThread, o);
  9. }
  10. return o;
  11. }

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

  15. public Object initialValue(){
  16. return null;
  17. }
  18. }


当然java的ThreadLocal实现的总体思路也大致如此。

我们看看jdk提供的api文档:

T get()

返回此线程局部变量的当前线程副本中的值,如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值

protected T initialValue()

返回此线程局部变量的当前线程的“初始值”。

void remove()

移除此线程局部变量当前线程的值。

void set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值。

我们看jdk文档提供了一个例子:

Java代码


  1. import java.util.concurrent.atomic.AtomicInteger;

  2. public class UniqueThreadIdGenerator {

  3. private static final AtomicInteger uniqueId = new AtomicInteger(0);

  4. private static final ThreadLocal < Integer > uniqueNum =
  5. new ThreadLocal < Integer > () {
  6. @Override protected Integer initialValue() {
  7. return uniqueId.getAndIncrement();
  8. }
  9. };

  10. public static int getCurrentThreadId() {
  11. return uniqueId.get();//应该是return uniqueNum.get();
  12. }
  13. } // UniqueThreadIdGenerator


这个例子我看的时候没看懂,其实是有错误的 return uniqueId.get();,应该是return uniqueNum.get();我用的是中文翻译过来的jdk api 1.6.0,不知道

大家的jdk帮助文档有没有这个问题.

我们看看Hibernate官方文档提供的一个通过ThreadLocal维护Session的例子:

Java代码


  1. public class HibernateUtil {
  2. private static final SessionFactory sessionFactory;
  3. static {
  4. try {
  5. sessionFactory = new Configuration().configure()
  6. .buildSessionFactory();

  7. } catch (Throwable ex) {
  8. ex.printStackTrace();
  9. throw new ExceptionInInitializerError(ex);

  10. }

  11. }
  12. public static final ThreadLocal tLocalsess = new ThreadLocal();
  13. // 取得session
  14. public static Session currentSession() {
  15. Session session = (Session) tLocalsess.get();
  16. // 打开一个新的session,如果当前的不可用.
  17. try {
  18. if (session == null || !session.isOpen()) {
  19. session = openSession();
  20. tLocalsess.set(session);
  21. }

  22. } catch (HibernateException e) {
  23. // 抛出HibernateException异常
  24. e.printStackTrace();

  25. }
  26. return session;
  27. }
  28. public static void closeSession() {
  29. Session session = (Session) tLocalsess.get();
  30. tLocalsess.set(null);
  31. try {
  32. if (session != null && session.isOpen()) {
  33. session.close();
  34. }
  35. } catch (HibernateException e) {
  36. //抛出 InfrastructureException异常
  37. }
  38. }
  39. //other code
  40. }
  41. 这段代码借助threadLocal,每一个线程保存一个session实例,从而避免线程内的频繁创建和销毁session.


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

用 ThreadLocal 管理每线程调试日志

Java代码


  1. public class DebugLogger {
  2. private static class ThreadLocalList extends ThreadLocal {
  3. public Object initialValue() {
  4. return new ArrayList();
  5. }
  6. public List getList() {
  7. return (List) super.get();
  8. }
  9. }
  10. private ThreadLocalList list = new ThreadLocalList();
  11. private static String[] stringArray = new String[0];
  12. public void clear() {
  13. list.getList().clear();
  14. }
  15. public void put(String text) {
  16. list.getList().add(text);
  17. }
  18. public String[] get() {
  19. return list.getList().toArray(stringArray);
  20. }
  21. }



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

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

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

当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。

下面是对ThreadLocal 类的源码分析下:

void set(Object value):设置当前线程的线程局部变量的值。 public Object get() :该方法返回当前线程所对应的线程局部变量。 (同一个线程获取同一个变量的引用) public void remove() :将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected Object initialValue():返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的(在上面的例子也可以看到,其实就是一个默认值的设置)。这个方法是一个延迟调用方法,在线程第1次调用 get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。


1、void set(Object value)方法分析

Java代码 ThreadLocal源代码分析_线程安全


  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. //查看当前线程中的ThreadLocalMap 是否存在。
  4. ThreadLocalMap map = getMap(t);
  5. //存在的话就把当前线程设置进ThreadLocalMap中。
  6. if (map != null)
  7. map.set(this, value);
  8. else
  9. //否则生成一个ThreadLocalMap 对象,并把值赋到当前线程的ThreadLocalMap中
  10. createMap(t, value);
  11. }
  12. //-------------------------------解析的代码分割线-------------------------------

  13. //返回当前线程的ThreadLocalMap
  14. ThreadLocalMap getMap(Thread t) {
  15. return t.threadLocals;
  16. }
  17. //创建新的ThreadLocalMap赋值到当前线程
  18. void createMap(Thread t, T firstValue) {
  19. t.threadLocals = new ThreadLocalMap(this, firstValue);
  20. }






下面说下关于ThreadLocalMap 类,这个类有点类似于WeakHashMap(这个类对于解决某些问题提供了很好的性能上的方案,有空再继续深入了解。),虽然ThreadLocalMap没有继承Map的,但它具有Map的一些特性,有关这些特性就不说了,主要说说它们的一个很大的共同点就是,它们的key 都是弱引用的对象,因此在该键没有被引用时,将会被JVM收集。(有关弱引用的解析,请参考我的博客上的:​​http://zhxing.iteye.com/admin/blogs/571007​​)

ThreadLocalMap是ThreadLocal的一个嵌套类,它的一些源码如下:

Java代码 ThreadLocal源代码分析_线程安全


  1. static class ThreadLocalMap {

  2. /**
  3. * The entries in this hash map extend WeakReference, using
  4. * its main ref field as the key (which is always a
  5. * ThreadLocal object). Note that null keys (i.e. entry.get()
  6. * == null) mean that the key is no longer referenced, so the
  7. * entry can be expunged from table. Such entries are referred to
  8. * as "stale entries" in the code that follows.
  9. */
  10. static class Entry extends WeakReference<ThreadLocal> {
  11. /** The value associated with this ThreadLocal. */
  12. Object value;

  13. Entry(ThreadLocal k, Object v) {
  14. super(k);
  15. value = v;
  16. }
  17. }



2、 public Object get() 方法分析

Java代码 ThreadLocal源代码分析_线程安全


  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. //这个是传入当前的ThreadLocal 对象引用获取所对应的值
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null)
  8. return (T)e.value;
  9. }
  10. //如果map 对象为空,说明是第一次调用这个方法,之前也没有调用set()方法
  11. return setInitialValue();
  12. }
  13. //-------------------------------解析的代码分割线-------------------------------
  14. //这个方法很简单就是调用initialValue()获取初始值(默认值),而且要赋值在当前线程的map中(相当于调用了set()方法)
  15. private T setInitialValue() {
  16. T value = initialValue();
  17. Thread t = Thread.currentThread();
  18. ThreadLocalMap map = getMap(t);
  19. if (map != null)
  20. map.set(this, value);
  21. else
  22. createMap(t, value);
  23. return value;
  24. }
  25. //ThreadLocal 的默认实现是返回null 的,所以如果在多个线程中使用时,如果需要用到默认值,那子类就应该覆盖该值
  26. protected T initialValue() {
  27. return null;
  28. }





3、public void remove() 方法

Java代码 ThreadLocal源代码分析_线程安全


  1. //这个方法没什么可说的,就是删除掉当前线程中的变量(释放内存)
  2. public void remove() {
  3. ThreadLocalMap m = getMap(Thread.currentThread());
  4. if (m != null)
  5. m.remove(this);
  6. }



总结



ThreadLocal 可以实现在同一个线程内共享一个变量,不同的线程不能共享,只能得到该变量的一个副本(就是一个默认的初始化值)。

在论坛中也有人提到ThreadLocal和synchronized 的问题,我觉得它们都是解决线程的两个不同的方案。ThreadLocal解决的是同一个线程内的资源共享问题,而synchronized 解决的是多个线程间的资源共享问题。

有关的讨论在:​​http://www.iteye.com/topic/179040​


有关这个类的应用,可以参考​​http://zhxing.iteye.com/admin/blogs/306070​

3. ThreadLocal在JavaEE中的应用

ThreadLocal在Hibernate中得到充分的使用,特别是对于事物的处理上。比如,getCurrentSession非常好,不需要我们自己写ThreadLocal只需要在hibernate.cfg的配置文件中声明一下便可获得ThreadLocal的好处是便于我们划分我们的程序的层次与封装,带也带来了一定的性能问题。特别是“如果你使用的是getCurrentSession,那么就算你是一个简单的select语句,也必须包含在事务块中”。碰到这种情况,可以采取如下手段:



  1. 一个service方法中只有单个dao操作且此操作是一个select类的操作,请使用openSession,并且即时在finally块中关闭它;
  2. 如果一个service方法中涉及到多个dao操作,请一定使用getCurrentSession;
  3. 如果一个service方法中混合着select操作,delete, update, insert操作。请按照下述原则:

a. 将属于select的操作,单独做成一个dao方法,该dao使用openSession并且在finally块中及时关闭session,

该dao只需要返回一个java的object如:List<Student>即可,如果出错将exception抛回给调用它的service

方法。

b. 对于其它的delete, insert, update的dao操作,请使用getCurrentSession。 c. 忌讳,把select类的操作放在“事务”中;




4. openSession与getCurrentSession的区别

a. openSession一旦被调用,必须且一定要在finally块中close,要不然你就等着out of memory吧;

b. 如果你使用的是getCurrentSession,那么你不能在finally块中调用”session.close()”,不行你可以在finally块中用try-catch把session.close();包起来,然后在catch{}块中抛出这个exception,这个exception将会是:sessionhas been already closed。其原因是你用的是getCurrentSession,那么它在session.commit()或者是session.rollback()时就已经调用了一次session.close()了,因此你只要正确放置session.commit()与rollback()即可。另外,你可以在finally块中调用”ThreadLocalSessionContext.unbind(factory);”,以使得当前的事务结束时把session(即dbconnection)还回db connection pool中。