ThreadLocal的理解和使用

1.ThreadLocal初步

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

主要是四个方法组成

initialValue(),get(),set(T),remove()

 

例子

Hibernate的文档时看到了关于使ThreadLocal管理多线程访问的部分。具体代码如下

 

  1. 1. public static final ThreadLocal session = new ThreadLocal(); 
  2. 2. public static Session currentSession() { 
  3. 3. Session s = (Session)session.get(); 
  4. 4. //open a new session,if this session has none 

  5. 5. if(== null){ 
  6. 6. s = sessionFactory.openSession(); 
  7. 7. session.set(s); 
  8. 8. } 
  9.       return s; 
  10. 9. }

1。初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。 
    如果不初始化initialvalue,则initialvalue返回null。 
3。session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的s(数据库连接)。 
5。如果是该线程初次访问,自然,s(数据库连接)会是null,接着创建一个Session,具体就是行6。 
6。创建一个数据库连接实例 s 
7。保存该数据库连接s到ThreadLocal中。 
8。如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。

 

当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal进行子类化,EasyDBO中创建jdbc连接上下文就是这样做的:

  1. public class JDBCContext{
  2.  private static Logger logger = Logger.getLogger(JDBCContext.class);
  3.  private DataSource ds;
  4.  protected Connection connection;
  5.  private boolean isValid = true;
  6.  private static ThreadLocal jdbcContext;

  7.  private JDBCContext(DataSource ds){
  8.   this.ds = ds;
  9.   createConnection(); 
  10.  }
  11.  public static JDBCContext getJdbcContext(javax.sql.DataSource ds)
  12.  { 
  13.   if(jdbcContext==null)jdbcContext=new JDBCContextThreadLocal(ds);
  14.   JDBCContext context = (JDBCContext) jdbcContext.get();
  15.   if (context == null) {
  16.    context = new JDBCContext(ds);
  17.   }
  18.   return context;
  19.  }

  20.  private static class JDBCContextThreadLocal extends ThreadLocal {
  21.   public javax.sql.DataSource ds;
  22.   public JDBCContextThreadLocal(javax.sql.DataSource ds)
  23.   {
  24.    this.ds=ds;
  25.   }
  26.   protected synchronized Object initialValue() {
  27.    return new JDBCContext(ds);
  28.   }
  29.  }
  30. }

 

使用单例模式,不同的线程调用getJdbcContext()获得自己的jdbcContext,都是通过JDBCContextThreadLocal 内置子类来获得JDBCContext对象的线程局部变量

 

 

一、ThreadLocal是java线程的一个实现
      ThreadLocal的确是和java线程有关,不过它并不是java线程的一个实现,它只是用来维护本地变量。针对每个线程,提供自己的变量版本,主要是为了避免线程冲突,每个线程维护自己的版本。彼此独立,修改不会影响到对方。

 二、ThreadLocal是相对于每个session的

        ThreadLocal顾名思义,是针对线程。在java web编程上,每个用户从开始到会话结束,都有自己的一个session标识。但是ThreadLocal并不是在会话层上。其实,Threadlocal是独立于用户session的。它是一种服务器端行为,当服务器每生成一个新的线程时,就会维护自己的ThreadLocal。对于这个误解,个人认为应该是开发人员在本地基于一些应用服务器测试的结果。众所周知,一般的应用服务器都会维护一套线程池,也就是说,对于每次访问,并不一定就新生成一个线程。而是自己有一个线程缓存池。对于访问,先从缓存池里面找到已有的线程,如果已经用光,才去新生成新的线程。所以,由于开发人员自己在测试时,一般只有他自己在测,这样服务器的负担很小,这样导致每次访问可能是共用同样一个线程,导致会有这样的误解:每个session有一个ThreadLocal

 三、ThreadLocal是相对于每个线程的,用户每次访问会有新的ThreadLocal

  理论上来说,ThreadLocal是的确是相对于每个线程,每个线程会有自己的ThreadLocal。但是上面已经讲到,一般的应用服务器都会维护一套线程池。因此,不同用户访问,可能会接受到同样的线程。因此,在做基于TheadLocal时,需要谨慎,避免出现ThreadLocal变量的缓存,导致其他线程访问到本线程变量

 四、对每个用户访问,ThreadLocal可以多用
        可以说,ThreadLocal是一把双刃剑,用得来的话可以起到非常好的效果。但是,ThreadLocal如果用得不好,就会跟全局变量一样。代码不能重用,不能独立测试。因为,一些本来可以重用的类,现在依赖于ThreadLocal变量。如果在其他没有ThreadLocal场合,这些类就变得不可用了。个人觉得ThreadLocal用得很好的几个应用场合,值得参考

  1、存放当前session用户:quake want的jert

  2、存放一些context变量,比如webwork的ActionContext

  3、存放session,比如Spring hibernate orm的session


2、ThreadLocal的理解

ThreadLocal是什么

  早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

  所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

  值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

  ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

[java]  view plain copy print ?
  1. package com.test;  
  2.   
  3. public class TestNum {  
  4.     // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值  
  5.     private static ThreadLocal seqNum = new ThreadLocal() {  
  6.         public Integer initialValue() {  
  7.             return 0;  
  8.         }  
  9.     };  
  10.   
  11.     // ②获取下一个序列值  
  12.     public int getNextNum() {  
  13.         seqNum.set(seqNum.get() + 1);  
  14.         return seqNum.get();  
  15.     }  
  16.   
  17.     public static void main(String[] args) {  
  18.         TestNum sn = new TestNum();  
  19.         // ③ 3个线程共享sn,各自产生序列号  
  20.         TestClient t1 = new TestClient(sn);  
  21.         TestClient t2 = new TestClient(sn);  
  22.         TestClient t3 = new TestClient(sn);  
  23.         t1.start();  
  24.         t2.start();  
  25.         t3.start();  
  26.     }  
  27.   
  28.     private static class TestClient extends Thread {  
  29.         private TestNum sn;  
  30.   
  31.         public TestClient(TestNum sn) {  
  32.             this.sn = sn;  
  33.         }  
  34.   
  35.         public void run() {  
  36.             for (int i = 0; i < 3; i++) {  
  37.                 // ④每个线程打出3个序列值  
  38.                 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["  
  39.                          + sn.getNextNum() + "]");  
  40.             }  
  41.         }  
  42.     }  
  43. }  


 通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:

thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]

考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。


Thread同步机制的比较

  ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:

ThreadLocal的理解和使用_第1张图片

  同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

  下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单3 TestDao:非线程安全

[java]  view plain copy print ?
  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.SQLException;  
  5. import java.sql.Statement;  
  6.   
  7. public class TestDao {  
  8.     private Connection conn;// ①一个非线程安全的变量  
  9.   
  10.     public void addTopic() throws SQLException {  
  11.         Statement stat = conn.createStatement();// ②引用非线程安全变量  
  12.         // …  
  13.     }  
  14. }  



由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TestDao:线程安全

[java]  view plain copy print ?
  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.SQLException;  
  5. import java.sql.Statement;  
  6.   
  7. public class TestDaoNew {  
  8.     // ①使用ThreadLocal保存Connection变量  
  9.     private static ThreadLocal connThreadLocal = new ThreadLocal();  
  10.   
  11.     public static Connection getConnection() {  
  12.         // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,  
  13.         // 并将其保存到线程本地变量中。  
  14.         if (connThreadLocal.get() == null) {  
  15.             Connection conn = getConnection();  
  16.             connThreadLocal.set(conn);  
  17.             return conn;  
  18.         } else {  
  19.             return connThreadLocal.get();// ③直接返回线程本地变量  
  20.         }  
  21.     }  
  22.   
  23.     public void addTopic() throws SQLException {  
  24.         // ④从ThreadLocal中获取线程对应的Connection  
  25.         Statement stat = getConnection().createStatement();  
  26.     }  
  27. }  


  不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

  当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。



ConnectionManager.java

[java]  view plain copy print ?
  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.DriverManager;  
  5. import java.sql.SQLException;  
  6.   
  7. public class ConnectionManager {  
  8.   
  9.     private static ThreadLocal connectionHolder = new ThreadLocal() {  
  10.         @Override  
  11.         protected Connection initialValue() {  
  12.             Connection conn = null;  
  13.             try {  
  14.                 conn = DriverManager.getConnection(  
  15.                         "jdbc:mysql://localhost:3306/test""username",  
  16.                         "password");  
  17.             } catch (SQLException e) {  
  18.                 e.printStackTrace();  
  19.             }  
  20.             return conn;  
  21.         }  
  22.     };  
  23.   
  24.     public static Connection getConnection() {  
  25.         return connectionHolder.get();  
  26.     }  
  27.   
  28.     public static void setConnection(Connection conn) {  
  29.         connectionHolder.set(conn);  
  30.     }  
  31. }  


java.lang.ThreadLocal的具体实现

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:

[java]  view plain copy print ?
  1. /** 
  2.     * Sets the current thread's copy of this thread-local variable 
  3.     * to the specified value.  Most subclasses will have no need to 
  4.     * override this method, relying solely on the {@link #initialValue} 
  5.     * method to set the values of thread-locals. 
  6.     * 
  7.     * @param value the value to be stored in the current thread's copy of 
  8.     *        this thread-local. 
  9.     */  
  10.    public void set(T value) {  
  11.        Thread t = Thread.currentThread();  
  12.        ThreadLocalMap map = getMap(t);  
  13.        if (map != null)  
  14.            map.set(this, value);  
  15.        else  
  16.            createMap(t, value);  
  17.    }  

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。


线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。


为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

[java]  view plain copy print ?
  1. /** 
  2.  * Get the map associated with a ThreadLocal. Overridden in 
  3.  * InheritableThreadLocal. 
  4.  * 
  5.  * @param  t the current thread 
  6.  * @return the map 
  7.  */  
  8. ThreadLocalMap getMap(Thread t) {  
  9.     return t.threadLocals;  
  10. }  
  11.   
  12. /** 
  13.  * Create the map associated with a ThreadLocal. Overridden in 
  14.  * InheritableThreadLocal. 
  15.  * 
  16.  * @param t the current thread 
  17.  * @param firstValue value for the initial entry of the map 
  18.  * @param map the map to store. 
  19.  */  
  20. void createMap(Thread t, T firstValue) {  
  21.     t.threadLocals = new ThreadLocalMap(this, firstValue);  
  22. }  

接下来再看一下ThreadLocal类中的get()方法:

[java]  view plain copy print ?
  1. /** 
  2.  * Returns the value in the current thread's copy of this 
  3.  * thread-local variable.  If the variable has no value for the 
  4.  * current thread, it is first initialized to the value returned 
  5.  * by an invocation of the {@link #initialValue} method. 
  6.  * 
  7.  * @return the current thread's value of this thread-local 
  8.  */  
  9. public T get() {  
  10.     Thread t = Thread.currentThread();  
  11.     ThreadLocalMap map = getMap(t);  
  12.     if (map != null) {  
  13.         ThreadLocalMap.Entry e = map.getEntry(this);  
  14.         if (e != null)  
  15.             return (T)e.value;  
  16.     }  
  17.     return setInitialValue();  
  18. }  

再来看setInitialValue()方法:

[java]  view plain copy print ?
  1. /** 
  2.     * Variant of set() to establish initialValue. Used instead 
  3.     * of set() in case user has overridden the set() method. 
  4.     * 
  5.     * @return the initial value 
  6.     */  
  7.    private T setInitialValue() {  
  8.        T value = initialValue();  
  9.        Thread t = Thread.currentThread();  
  10.        ThreadLocalMap map = getMap(t);  
  11.        if (map != null)  
  12.            map.set(this, value);  
  13.        else  
  14.            createMap(t, value);  
  15.        return value;  
  16.    }  

  获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。


  进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。


小结

  ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ConnectionManager.java

[java]  view plain copy print ?
  1. package com.test;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.DriverManager;  
  5. import java.sql.SQLException;  
  6.   
  7. public class ConnectionManager {  
  8.   
  9.     private static ThreadLocal connectionHolder = new ThreadLocal() {  
  10.         @Override  
  11.         protected Connection initialValue() {  
  12.             Connection conn = null;  
  13.             try {  
  14.                 conn = DriverManager.getConnection(  
  15.                         "jdbc:mysql://localhost:3306/test""username",  
  16.                         "password");  
  17.             } catch (SQLException e) {  
  18.                 e.printStackTrace();  
  19.             }  
  20.             return conn;  
  21.         }  
  22.     };  
  23.   
  24.     public static Connection getConnection() {  
  25.         return connectionHolder.get();  
  26.     }  
  27.   
  28.     public static void setConnection(Connection conn) {  
  29.         connectionHolder.set(conn);  
  30.     }  


3、ThreadLocal封装Connection,实现同一线程共享资源


 线程安全一直是程序猿们关注的焦点,多线程也一直是比较让人头疼的话题,想必大家曾经也遇到过各种各种的问题,我就不再累述了。当然,解决方式也有很多,这篇博文给大家提供一种很好的解决线程安全问题的思路。

 

      首先,我们先简单的认识一下ThreadLocal,之后是实例+解析,最后一句话总结。


1、认识一下ThreaLocal


       认识ThreadLocal必须要通过api文档,不仅仅具有说服力,而且它会给你更加全面的解释。下面我我给大家从api文档上截取一张图,并标出来了七点需要重点理解的内容,实例过后的解析也是重点解释这七部分。


ThreadLocal的理解和使用_第2张图片


      对于上面的内容,不理解没有关系,我们通过下面的实例加深一下理解,实例之后我会给大家一个更加深入的解释。


2、ThreaLocal封装Connection实例+解析


       下面的代码只是ThreaLocal封装Connection的核心代码,对于多余的内容成功避开就好,并且有一部分代码是“dom4j解析xml文件,连接数据库”的内容,非常适合初学者,如有需要,请您移驾到此。


[java]  view plain copy print ?
  1. package com.bjpowernode.drp.util;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.DriverManager;  
  5. import java.sql.ResultSet;  
  6. import java.sql.SQLException;  
  7. import java.sql.Statement;  
  8.   
  9. /** 
  10.  * 采用ThreadLocal封装Connection 
  11.  * 只要线程是活动的,没有结束,ThreadLocal是可访问的,就可以访问本线程的connection 
  12.  *  
  13.  * @author liang 
  14.  * 
  15.  */  
  16. public class ConnectionManager {  
  17.   
  18.     //使用ThreadLocal保存Connection变量  
  19.     private static ThreadLocal connectionHolder = new ThreadLocal();  
  20.       
  21.     /** 
  22.      * 连接Connection 
  23.      * @return 
  24.      */  
  25.     public static Connection getConnection(){  
  26.         //ThreadLocal取得当前线程的connection  
  27.         Connection conn = connectionHolder.get();  
  28.         //如果ThreadLocal没有绑定相应的Connection,创建一个新的Connection,  
  29.         //并将其保存到本地线程变量中。  
  30.         if(conn == null){  
  31.             try {  
  32.                 JdbcConfig jdbcConfig = XmlConfigReader.getInstance().getJdbcConfig();  
  33.                 Class.forName(jdbcConfig.getDriverName());                
  34.                 conn = DriverManager.getConnection(jdbcConfig.getUrl(), jdbcConfig.getUserName(), jdbcConfig.getPassword());  
  35.                 //将当前线程的Connection设置到ThreadLocal  
  36.                 connectionHolder.set(conn);  
  37.             } catch (ClassNotFoundException e) {  
  38.                 e.printStackTrace();  
  39.                 throw new ApplicationException("系统错误,请联系系统管理员");  
  40.             } catch (SQLException e) {  
  41.                 e.printStackTrace();  
  42.             throw new ApplicationException("系统错误,请联系系统管理员");  
  43.             }  
  44.         }  
  45.         return conn;                                      
  46.           
  47.     }  
  48.     /** 
  49.      * 关闭Connection,清除集合中的Connection 
  50.      */  
  51.     public static void closeConnection(){  
  52.         //ThreadLocal取得当前线程的connection  
  53.         Connection conn = connectionHolder.get();  
  54.         //当前线程的connection不为空时,关闭connection.  
  55.         if(conn != null){  
  56.             try{  
  57.                 conn.close();  
  58.                 //connection关闭之后,要从ThreadLocal的集合中清除Connection  
  59.                 connectionHolder.remove();  
  60.             }catch(SQLException e){  
  61.                 e.printStackTrace();  
  62.             }  
  63.   
  64.         }  
  65.     }  
  66. }  


      下面的代码给大家演示了:ThreadLocal如何在同一个线程中可以共享Connection资源。


[java]  view plain copy print ?
  1. package com.bjpowernode.drp.flowcard.manager.impl;  
  2.   
  3. import java.sql.Connection;  
  4. import java.util.Date;  
  5. import com.bjpowernode.drp.flowcard.dao.FlowCardDao;  
  6. import com.bjpowernode.drp.flowcard.domain.FlowCard;  
  7. import com.bjpowernode.drp.flowcard.manager.FlowCardManager;  
  8. import com.bjpowernode.drp.util.ApplicationException;  
  9. import com.bjpowernode.drp.util.BeanFactory;  
  10. import com.bjpowernode.drp.util.ConnectionManager;  
  11. import com.bjpowernode.drp.util.DaoException;  
  12. import com.bjpowernode.drp.util.PageModel;  
  13.   
  14. public class FlowCardManagerImpl implements FlowCardManager {  
  15.   
  16.       
  17.     private FlowCardDao flowCardDao;  
  18.     //构造函数  
  19.     public FlowCardManagerImpl(){  
  20.         this.flowCardDao = (FlowCardDao) BeanFactory.getInstance().getDaoObject(FlowCardDao.class);  
  21.     }  
  22.       
  23.     @Override  
  24.     public void addFlowCard(FlowCard flowCard) throws ApplicationException {  
  25.           
  26.         Connection conn = null;  
  27.         try{  
  28.             //从ThreadLocal中获取线程对应的Connection  
  29.             conn = ConnectionManager.getConnection();  
  30.             //开始事务  
  31.             ConnectionManager.beginTransaction(conn);  
  32.             //生成流向单单号  
  33.             String flowCardVouNo = flowCardDao.generateVouNo();  
  34.             //添加流向单主信息  
  35.             flowCardDao.addFlowCardMaster(flowCardVouNo, flowCard);  
  36.             //添加流向单明细信息  
  37.             flowCardDao.addFlowCardDetail(flowCardVouNo, flowCard.getFlowCardDetailList());  
  38.             //提交事务  
  39.             ConnectionManager.commitTransaction(conn);        
  40.         }catch(DaoException e){  
  41.             //回滚事务  
  42.             ConnectionManager.rollbackTransaction(conn);  
  43.             throw new ApplicationException("添加流向单失败!");  
  44.         }finally{  
  45.             //关闭Connection并从ThreadLocal集合中清除  
  46.             ConnectionManager.closeConnection();  
  47.         }  
  48.       
  49.     }  
  50. }  

解析:

 

1、该类提供了线程局部变量,它独立于变量的初始化副本

 

       大家可能对局部变量不太理解,为什么不是成员变量或全局变量,此时就涉及到变量的作用域问题。ThreadLocal具有比局部变量更大一点的作用域,在此作用域内资源可以共享,线程是安全的。

       我们还了解到ThreadLocal并不是本地线程,而是一个线程变量,它只是用来维护本地变量。针对每个线程提供自己的变量版本,避免了多线程的冲突问题,每个线程只需要维护自己的版本就好,彼此独立,不会影响到对方。

 

2、每个线程有自己的一个ThreadLocal,修改它并不影响其他线程

  

      我们根据下面这张图可以看到,向ThreadLocal里面存东西就是创建了一个Map,一个线程对应一个Map集合,然后ThreadLocal把这个Map挂到当前的线程底下,一个key值对应一个value,这样Map就只属于当前线程。(如果您不理解Map的特点可以猛戳)

ThreadLocal的理解和使用_第3张图片



3、在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。 

 

      上面我们知道了变量副本的存放在了map中,当我们不在调用set,此时不在将引用指向该‘map’,而本线程退出时会执行资源回收操作,将申请的资源进行回收,其实就是将引用设置为null。这时已经不在有任何引用指向该map,故而会被垃圾回收。


3、对比ThreadLocal和synchronized同步机制


相同点:

        1、ThreadLocal和线程同步机制都能解决多线程中相同变量的访问冲突问题。

不同点:

       1、适用的情况不同

 

        在同步机制中,使用同步保证同一时间只有一个线程访问,不能同时访问共享资源,否则就是出现错误。ThreadLocal则隔离了相关的资源,并在同一个线程中可以共享这个资源。彼此独立,修改不会影响到对方。

 

       2、最终实现的效果不同

    

       对于多线程资源共享问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

 

      上面博客的链接同样也是线程同步机制synchronized的实例,大家可以通过两个实例体会一下它们的异同点,再加上异同点解析,相信您对它们已经有了很深刻的认识。


4、一句话总结ThreadLocal


       ThreadLocal是解决线程安全问题一个很好的思路,在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,并且程序拥有更高的并发性。



4、ThreadLocal的使用一


首先有几点需要大家清楚的:

1、  ThreadLocal只是对需要存储的对象的管理,而存储实际是由当前Thread负责。个人理解为ThreadLocal是一个操作Thread. threadLocals 的工具。

2、  使用ThreadLocal可以使对象达到线程隔离的目的。同一个ThreadLocal操作不同的Thread,实质是各个Thread对自己的变量操作。

3、  为什么要使用ThreadLocal,个人感觉有两个原因,1是与其它线程的隔离,2是可以在一个线程的生命周期中使用同一个对象,达到对象传递的作用。这样的好处是可以减少dal访问或者ws调用。

 

我这里列出一个用到ThreadLocal的例子,主要的作用是使用ThreadLocal记录用户信息以及记录用户的执行时间。这在实际应用中,可以映射为全局记录用户的权限,以及使用Threadlocal对系统的性能做一些分析等。。

 

首先有两个对象,一个是用户对象

Java代码   收藏代码
  1. // 简单记录用户是否可以访问,可以用于全局权限控制等  
  2. class User {  
  3.     private String name;  
  4.     private boolean isAllow;  
  5.     public User(String name, boolean isAllow) {  
  6.         this.name = name;  
  7.         this.isAllow = isAllow;  
  8.     }  
  9.     public String getName() {  
  10.         return name;  
  11.     }  
  12.     public boolean isAllow() {  
  13.         return isAllow;  
  14.     }  
  15.     @Override  
  16.     public String toString() {  
  17.         return "用户名:" + name + "\t 是否允许访问:" + isAllow;  
  18.     }  
  19. }  

 

另一个是消费时间对象

 

Java代码   收藏代码
  1. // 用于记录每一步骤耗时…,可以用于每一步的性能分析  
  2. class TimeConsumer {  
  3.     // 名称  
  4.     private String name;  
  5.     // 耗时数据列表  
  6.     private List steps;  
  7.     public TimeConsumer(String name, long start) {  
  8.         this.name = name;  
  9.         steps = new ArrayList();  
  10.         steps.add(start);  
  11.     }  
  12.     public void andStep(long step) {  
  13.         steps.add(step);  
  14.     }  
  15.     @Override  
  16.     public String toString() {  
  17.         StringBuffer br = new StringBuffer("操作[" + name + "]共有"  
  18.                 + (steps.size() - 1) + "步\n");  
  19.         for (int i = 1; i < steps.size(); i++) {  
  20.             br.append("\t|--耗时[" + (steps.get(i) - steps.get(0))  
  21.                     + "ms]\n");  
  22.         }  
  23.         br.append("\n");  
  24.         return br.toString();  
  25.     }  
  26. }  

 

 

 

接下来,建立一个对这两个对象管理的ThreadLocal的类

Java代码   收藏代码
  1. // threadlocal 管理类  
  2. class MyThreadLocal {  
  3.     // 用于全局记录user访问权限  
  4.     private ThreadLocal userLocal;  
  5.     // 用于全局记录用户每一步的耗时  
  6.     private ThreadLocal timeLocal;  
  7.     private static MyThreadLocal local = new MyThreadLocal();  
  8.     private MyThreadLocal() {  
  9.         userLocal = new ThreadLocal();  
  10.         timeLocal = new ThreadLocal();  
  11.     }  
  12.     public static MyThreadLocal getInstanse() {  
  13.         return local;  
  14.     }  
  15.     public void addUser(User user) {  
  16.         userLocal.set(user);  
  17.     }  
  18.     public User getUser() {  
  19.         return userLocal.get();  
  20.     }  
  21.     public void addTime(TimeConsumer timeConsumer) {  
  22.         timeLocal.set(timeConsumer);  
  23.     }  
  24.     public void addTime(long l) {  
  25.         TimeConsumer time = timeLocal.get();  
  26.         timeLocal.remove();  
  27.         time.andStep(l);  
  28.         timeLocal.set(time);  
  29.     }  
  30.     public TimeConsumer getTime() {  
  31.         return timeLocal.get();  
  32.     }  
  33. }  

 

 

 

 

接下来就可以对Threadlocal进行测试了。为了模拟多线程,我这里自己实现了多线程

Java代码   收藏代码
  1. public class CoreThreadLocal {  
  2.     public static void main(String[] args) {  
  3.         new Thread(new TestRunnable("name1", 1000L, true)).start();  
  4.         new Thread(new TestRunnable("name2", 700L, true)).start();  
  5.         new Thread(new TestRunnable("name3"888false)).start();  
  6.     }  
  7. }  
  8.   
  9. // 用于测试,多线程实现  
  10. class TestRunnable implements Runnable {  
  11.     String name;  
  12.     long l;  
  13.     boolean isAllow;  
  14.   TestRunnable(String name, long l, boolean isAllow) {  
  15.         this.name = name;  
  16.         this.l = l;  
  17.         this.isAllow = isAllow;  
  18.     }  
  19.     public void run() {  
  20.         MyThreadLocal local = MyThreadLocal.getInstanse();  
  21.         local.addUser(new User(name, isAllow));  
  22.         local.addTime(new TimeConsumer(name, System.currentTimeMillis()));  
  23.         // 做某个业务,并记录时间  
  24.         doThings(l);  
  25.         local.addTime(System.currentTimeMillis());  
  26.         // 做某个业务,并记录时间  
  27.         doThings(l);  
  28.         local.addTime(System.currentTimeMillis());  
  29.         // 业务做完,打印日志  
  30.         System.out.println(local.getUser());  
  31.         System.out.println(local.getTime());  
  32.     }  
  33.     // 模拟具体业务的处理步骤  
  34.     private void doThings(long l) {  
  35.         try {  
  36.             Thread.sleep(l);  
  37.         } catch (InterruptedException e) {  
  38.             e.printStackTrace();  
  39.         }  
  40.     }  
  41. }  

 

 

 

 

运行上面的程序得到结果如下:

 

 

Java代码   收藏代码
  1. 用户名:name2    是否允许访问:true  
  2. 操作[name2]共有2步  
  3.     |--耗时[703ms]  
  4.     |--耗时[1406ms]  
  5.   
  6.   
  7. 用户名:name3    是否允许访问:false  
  8. 操作[name3]共有2步  
  9.     |--耗时[891ms]  
  10.     |--耗时[1781ms]  
  11.   
  12.   
  13. 用户名:name1    是否允许访问:true  
  14. 操作[name1]共有2步  
  15.     |--耗时[1000ms]  
  16.     |--耗时[2000ms]  

 

 


通过上面的测试程序,可以大概了解ThreadLocal的使用方法以及作用。

如果要深入使用,建议还是看下源码吧…


5、ThreadLocal类的使用二


ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。

我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。

虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。

按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但SpringDAO模板类并未采用线程同步机制,因为线程同步限制了并发访问,会带来很大的性能损失。

此外,通过代码同步解决性能安全问题挑战性很大,可能会增强好几倍的实现难度。那模板类究竟仰丈何种魔法神功,可以在无需同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal

ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。

ThreadLocal是什么

早在JDK 1.2的版本中就提供java.lang.ThreadLocalThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal很容易让人望文生义,想当然地认为是一个本地线程。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

线程局部变量并不是Java的新发明,很多语言(如IBM IBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

· void set(Object value) 

设置当前线程的线程局部变量的值。

· public Object get() 

该方法返回当前线程所对应的线程局部变量。

· public void remove() 

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

· protected Object initialValue() 

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null

值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocalAPI方法也相应进行了调整,新版本的API方法分别是void set(T value)T get()以及T initialValue()

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

代码清单1 SimpleThreadLocal

public class SimpleThreadLocal {

private Map valueMap = Collections.synchronizedMap(new HashMap());

public void set(Object newValue) {

valueMap.put(Thread.currentThread(), newValue);键为线程对象,值为本线程的变量副本

}

public Object get() {

Thread currentThread = Thread.currentThread();

Object o = valueMap.get(currentThread);返回本线程对应的变量

if (o == null && !valueMap.containsKey(currentThread)) {如果在Map中不存在,放到Map

中保存起来。

o = initialValue();

valueMap.put(currentThread, o);

}

return o;

}

public void remove() {

valueMap.remove(Thread.currentThread());

}

public Object initialValue() {

return null;

}

}

虽然代码清单93这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。

一个TheadLocal实例

下面,我们通过一个具体的实例了解一下ThreadLocal的具体使用方法。

代码清单2 SequenceNumber

package com.baobaotao.basic;

public class SequenceNumber {

通过匿名内部类覆盖ThreadLocalinitialValue()方法,指定初始值

private static ThreadLocal seqNum = new ThreadLocal(){

public Integer initialValue(){

return 0;

}

};

获取下一个序列值

public int getNextNum(){

seqNum.set(seqNum.get()+1);

return seqNum.get();

}

public static void main(String[] args) 

{

SequenceNumber sn = new SequenceNumber();

 3个线程共享sn,各自产生序列号

TestClient t1 = new TestClient(sn);

TestClient t2 = new TestClient(sn);

TestClient t3 = new TestClient(sn);

t1.start();

t2.start();

t3.start();

}

private static class TestClient extends Thread

{

private SequenceNumber sn;

public TestClient(SequenceNumber sn) {

this.sn = sn;

}

public void run()

{

for (int i = 0; i < 3; i++) {每个线程打出3个序列值

System.out.println("thread["+Thread.currentThread().getName()+

"] sn["+sn.getNextNum()+"]");

}

}

}

}

通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中处所示。TestClient线程产生一组序列号,在处,我们生成3TestClient,它们共享同一个SequenceNumber实例。运行以上代码,在控制台上输出以下的结果:

thread[Thread-2] sn[1]

thread[Thread-0] sn[1]

thread[Thread-1] sn[1]

thread[Thread-2] sn[2]

thread[Thread-0] sn[2]

thread[Thread-1] sn[2]

thread[Thread-2] sn[3]

thread[Thread-0] sn[3]

thread[Thread-1] sn[3]

考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个SequenceNumber实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

Thread同步机制的比较

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal

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

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。

概括起来说,对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图92所示:

1同一线程贯通三层

这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单3 TopicDao:非线程安全

public class TopicDao {

private Connection conn;一个非线程安全的变量

public void addTopic(){

Statement stat = conn.createStatement();引用非线程安全变量

}

}

由于处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocalconn这个非线程安全的状态进行改造:

代码清单4 TopicDao:线程安全

import java.sql.Connection;

import java.sql.Statement;

public class TopicDao {

使用ThreadLocal保存Connection变量

private static ThreadLocal connThreadLocal = new ThreadLocal();

public static Connection getConnection(){

如果connThreadLocal没有本线程对应的Connection创建一个新的Connection

并将其保存到线程本地变量中。

if (connThreadLocal.get() == null) {

Connection conn = ConnectionManager.getConnection();

connThreadLocal.set(conn);

return conn;

}else{

return connThreadLocal.get();直接返回线程本地变量

}

}

public void addTopic() {

ThreadLocal中获取线程对应的Connection

Statement stat = getConnection().createStatement();

}

}

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

当然,这个例子本身很粗糙,将ConnectionThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。

小结

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

 

 

 

 

最后   ,提供个Myeclipse对Hibernate自动生成的建立会化工厂类(HibernateSessionFactory.java),此类就是用ThreadLocal起到了每个线程拥有完全独立的Session对象的作用。



ThreadLocal设计模式


线程安全问题的由来

  在传统的Web开发中,我们处理Http请求最常用的方式是通过实现Servlet对象来进行Http请求的响应.Servlet是J2EE的重要标准之一,规定了Java如何响应Http请求的规范.通过HttpServletRequest和HttpServletResponse对象,我们能够轻松地与Web容器交互.

  当Web容器收到一个Http请求时,Web容器中的一个主调度线程会从事先定义好的线程中分配一个当前工作线程,将请求分配给当前的工作线程,由该线程来执行对应的Servlet对象中的service方法.当这个工作线程正在执行的时候,Web容器收到另外一个请求,主调度线程会同样从线程池中选择另外一个工作线程来服务新的请求.Web容器本身并不关心这个新的请求是否访问的是同一个Servlet实例.因此,我们可以得出一个结论:对于同一个Servlet对象的多个请求,Servlet的service方法将在一个多线程的环境中并发执行.所以,Web容器默认采用单实例(单Servlet实例)多线程的方式来处理Http请求.这种处理方式能够减少新建Servlet实例的开销,从而缩短了对Http请求的响应时间.但是,这样的处理方式会导致变量访问的线程安全问题.也就是说,Servlet对象并不是一个线程安全的对象.

下面的测试代码将证实这一点:
[java]  view plain copy print ?
  1. package com.qingdao.proxy;  
  2.   
  3. public class ThreadSafeTestServlet extends HttpServlet {  
  4.     // 定义一个实例变量,并非一个线程安全的变量  
  5.     private int counter = 0;  
  6.   
  7.     public void doGet(HttpServletRequest req, HttpServletResponse resp)  
  8.             throws ServletException, IOException {  
  9.         doPost(req, resp);  
  10.     }  
  11.   
  12.     public void doPost(HttpServletRequest req, HttpServletResponse resp)  
  13.             throws ServletException, IOException {  
  14.         // 输出当前Servlet的信息以及当前线程的信息  
  15.         System.out.println(this + ":" + Thread.currentThread());  
  16.         // 循环,并增加实例变量counter的值  
  17.         for (int i = 0; i < 5; i++) {  
  18.             System.out.println("Counter = " + counter);  
  19.             try {  
  20.                 Thread.sleep((long) Math.random() * 1000);  
  21.                 counter++;  
  22.             } catch (InterruptedException exc) {  
  23.             }  
  24.         }  
  25.     }  
  26. }  

 
输出结果为:
sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor23,5,main] 
Counter = 60 
Counter = 61 
Counter = 62 
Counter = 65 
Counter = 68 
Counter = 71 
Counter = 74 
Counter = 77 
Counter = 80 
Counter = 83 

sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor22,5,main] 
Counter = 61 
Counter = 63 
Counter = 66 
Counter = 69 
Counter = 72 
Counter = 75 
Counter = 78 
Counter = 81 
Counter = 84 
Counter = 87 

sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor24,5,main] 
Counter = 61 
Counter = 64 
Counter = 67 
Counter = 70 
Counter = 73 
Counter = 76 
Counter = 79 
Counter = 82 
Counter = 85 
Counter = 88

 

 
   
通过上面的输出 , 我们可以得出以下三个Servlet对象的运行特性:1. Servlet对象是一个无状态的单例对象(Singleton), 因为我们看到多次请求的this指针所打印出来的hashcode值都相同2. Servlet在不同的线程(线程池)中运行,如http-8081-Processor22和http-8081-Processor23等输出值可以明显区分出不同的线程执行了同一段Servlet逻辑代码. 3. Counter变量在不同的线程中共享 ,而且它的值被不同的线程修改,输出时已经不是顺序输出.也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的.
 
 
 
ThreadLocal模式的实现机理
 
    

在JDK的早期版本中,提供了一种解决多线程并发问题的方案: java.lang.ThreadLocal类.ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题.
ThreadLocal本身并不是一个线程,而是通过操作当前线程(Thread)中的一个内部变量来达到与其他线程隔离的目的.之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程(Thread)的一个本地变量.如果我们看一下Thread的源码实现,就会发现这一变量,代码如下:
[java]  view plain copy print ?
  1. public class Thread implements Runnable {  
  2.     // 这里省略了许多其他的代码  
  3.     ThreadLocal.ThreadLocalMap threadLocals = null;  
  4. }  


 


这是JDK中Thread源码的一部分,从中我们可以看出ThreadLocalMap跟随着当前的线程而存在.不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量,这也就是“副本”的含义.接下来我们再来看看ThreadLocal.ThreadLocalMap是如何定义的,以及ThreadLocal如何来操作它
 
[java]  view plain copy print ?
  1. public class ThreadLocal {  
  2.   
  3.     // 这里省略了许多其他代码  
  4.   
  5.     // 将value的值保存于当前线程的本地变量中  
  6.     public void set(T value) {  
  7.         // 获取当前线程  
  8.         Thread t = Thread.currentThread();  
  9.         // 调用getMap方法获得当前线程中的本地变量ThreadLocalMap  
  10.         ThreadLocalMap map = getMap(t);  
  11.         // 如果ThreadLocalMap已存在,直接使用  
  12.         if (map != null)  
  13.             // 以当前的ThreadLocal的实例作为key,存储于当前线程的  
  14.             // ThreadLocalMap中,如果当前线程中被定义了多个不同的ThreadLocal  
  15.             // 的实例,则它们会作为不同key进行存储而不会互相干扰  
  16.             map.set(this, value);  
  17.         else  
  18.             // ThreadLocalMap不存在,则为当前线程创建一个新的  
  19.             createMap(t, value);  
  20.     }  
  21.   
  22.     // 获取当前线程中以当前ThreadLocal实例为key的变量值  
  23.     public T get() {  
  24.         // 获取当前线程  
  25.         Thread t = Thread.currentThread();  
  26.         // 获取当前线程中的ThreadLocalMap  
  27.         ThreadLocalMap map = getMap(t);  
  28.         if (map != null) {  
  29.             // 获取当前线程中以当前ThreadLocal实例为key的变量值  
  30.             ThreadLocalMap.Entry e = map.getEntry(this);  
  31.             if (e != null)  
  32.                 return (T) e.value;  
  33.         }  
  34.         // 当map不存在时,设置初始值  
  35.         return setInitialValue();  
  36.     }  
  37.   
  38.     // 从当前线程中获取与之对应的ThreadLocalMap  
  39.     ThreadLocalMap getMap(Thread t) {  
  40.         return t.threadLocals;  
  41.     }  
  42.   
  43.     // 创建当前线程中的ThreadLocalMap  
  44.     void createMap(Thread t, T firstValue) {  
  45.         // 调用构造函数生成当前线程中的ThreadLocalMap  
  46.         t.threadLocals = new ThreadLocalMap(this, firstValue);  
  47.     }  
  48.   
  49.     // ThreadLoaclMap的定义  
  50.     static class ThreadLocalMap {  
  51.         // 这里省略了许多代码  
  52.     }  
  53. }  

 
   
从上述代码中,我们看到了ThreadLocal类的大致结构和进行ThreadLocalMap的操作. 我们可以从中得出以下的结论:1. ThreadLocalMap变量属于线程(Thread)的内部属性,不同的线程(Thread)拥有完全不同的ThreadLocalMap变量. 2. 线程(Thread)中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的 . 3. 在创建ThreadLocalMap之前 ,会首先检查当前线程(Thread)中的ThreadLocalMap变量是否已经存在,如果不存在则创建一个;如果已经存在,则使用当前线程(Thread)已创建的ThreadLocalMap. 4. 使用当前线程(Thread)的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key进行存储 ThreadLocal模式,至少从两个方面完成了数据访问隔离,有了横向和纵向的两种不同的隔离方式, ThreadLocal模式就能真正地做到线程安全:纵向隔离 —— 线程(Thread)与线程(Thread)之间的数据访问隔离.这一点由线程(Thread)的数据结构保证.因为每个线程(Thread)在进行对象访问时,访问的都是各自线程自己的ThreadLocalMap.横向隔离 —— 同一个线程中,不同的ThreadLocal实例操作的对象之间的相互隔离.这一点由ThreadLocalMap在存储时,采用当前ThreadLocal的实例作为key来保证.

 

 

深入比较TheadLocal模式与synchronized关键字

  ThreadLocal模式synchronized关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同.

  1)ThreadLocal是一个java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题.所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本.

  2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性.在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量.此时,被用作“锁机制”的变量时多个线程共享的.

  同步机制(synchronized关键字)采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问.而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响

ThreadLocal模式的核心元素

要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例(也就是执行示意图中的菱形部分).而这一点,我们可以通过类的静态实例变量来实现,这个用于承载静态实例变量的类就被视作是一个共享环境.我们来看一个例子,如代码清单如下所示:

[java]  view plain copy print ?
  1. public class Counter {  
  2.      //新建一个静态的ThreadLocal变量,并通过get方法将其变为一个可访问的对象   
  3.      private static ThreadLocal counterContext = new ThreadLocal(){  
  4.          protected synchronized Integer initialValue(){  
  5.              return 10;  
  6.          }  
  7.      };  
  8.      // 通过静态的get方法访问ThreadLocal中存储的值  
  9.      public static Integer get(){  
  10.          return counterContext.get();  
  11.      }  
  12.      // 通过静态的set方法将变量值设置到ThreadLocal中  
  13.      public static void set(Integer value) {    
  14.          counterContext.set(value);    
  15.      }   
  16.      // 封装业务逻辑,操作存储于ThreadLocal中的变量    
  17.      public static Integer getNextCounter() {    
  18.          counterContext.set(counterContext.get() + 1);    
  19.          return counterContext.get();    
  20.      }    
  21.  }  


 

[java]  view plain copy print ?
  1. public class ThreadLocalTest extends Thread {  
  2.      public void run(){  
  3.          for(int i = 0; i < 3; i++){    
  4.              System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());    
  5.          }    
  6.      }  
  7.  }  


 

[java]  view plain copy print ?
  1. public class Test {  
  2.      public static void main(String[] args) {  
  3.          ThreadLocalTest testThread1 = new ThreadLocalTest();    
  4.          ThreadLocalTest testThread2 = new ThreadLocalTest();    
  5.          ThreadLocalTest testThread3 = new ThreadLocalTest();    
  6.          testThread1.start();    
  7.          testThread2.start();    
  8.          testThread3.start();  
  9.      }  
  10.  }  


输出结果:

Thread[Thread-2],counter=11
Thread[Thread-2],counter=12
Thread[Thread-2],counter=13
Thread[Thread-0],counter=11
Thread[Thread-0],counter=12
Thread[Thread-0],counter=13
Thread[Thread-1],counter=11
Thread[Thread-1],counter=12
Thread[Thread-1],counter=13

 

上面的输出结果也证实了,counter的值在多线程环境中的访问是线程安全的.从对例子的分析中我们可以再次体会到,ThreadLocal模式最合适的使用场景:在同一个线程(Thread)的不同开发层次中共享数据.

  从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
  1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,
使其成为一个共享数据环境.
  2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值).


  建立在ThreadLocal模式的实现步骤之上,ThreadLocal的使用则更加简单.在线程执行的任何地方,我们都可以通过访问共享数据类中所提供的ThreadLocal变量的设值和取值方法安全地获得当前线程中安全的变量值.
  这两个步骤,我们之后会在Struts2的实现中多次提及,读者只要能充分理解ThreadLocal处理多线程访问的基本原理,就能对Struts2的数据访问和数据共享的设计有一个整体的认识.

  讲到这里,我们回过头来看看ThreadLocal模式的引入,到底对我们的编程模型有什么重要的意义呢?

  结论 :使用ThreadLocal模式,可以使得数据在不同的编程层次得到有效地共享,

  这一点,是由ThreadLocal模式的实现机理决定的.因为实现ThreadLocal模式的一个重要步骤,就是构建一个静态的共享存储空间.从而使得任何对象在任何时刻都可以安全地对数据进行访问.

  结论 使用ThreadLocal模式,可以对执行逻辑与执行数据进行有效解耦

  这一点是ThreadLocal模式给我们带来的最为核心的一个影响,因为在一般情况下,Java对象之间的协作关系,主要通过参数和返回值来进行消息传递,这也是对象协作之间的一个重要依赖,而ThreadLocal模式彻底打破了这种依赖关系,通过线程安全的共享对象来进行数据共享,可以有效避免在编程层次之间形成数据依赖,这也成为了XWork事件处理体系设计的核心.


你可能感兴趣的:(JavaEE,Java高级学习)