ThreadLocal模式

1、线程安全问题的由来

  在传统的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对象并不是一个线程安全的对象。

有关线程安全的概念范畴

   谈到线程安全,去多初学者很容易在概念上混淆。线程安全,指的是在多线程环境下,一个类在执行某个方法时,对类的内部实例变量的访问时安全的。因此,对于下面列出来的2类变量,不存在任何线程安全的说法:

  1)方法签名中的任何参数变量。

  2)处于方法内部的局部变量。

  任何针对上述形式的变量的访问都是线程安全的,因为他们都是处于方法的内部,由当前的执行线程独自管理。

  这就是线程安全问题的由来:在传统的基于Servlet的开发模式中,Servlet对象内部的实例变量不是线程安全。在多线程环境中,这些变量的访问需要通过特殊的手段进行访问控制。

  解决线程安全访问的方法很多,比较容易想到的一种方案是使用同步机制(Synchronized),但是出于对Web应用效率的考虑,这种机制在Web开发中的可行性很低,也违背了Servlet的设计初衷。因此,我们需要另辟蹊径来解决这一困扰我们的问题。

 

 

ThreadLocal模式的实现机理

   在JDK的早期版本中,提供了一种解决多线程并发问题的方案:java.lang.ThreadLocal类。ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。

  ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

  ThreadLocal本身并不是一个线程,而是通过操作当前线程中的一个内部变量来达到与其他线程隔离的目的。之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程的一个本地变量。如果我们看一下Thread的源码实现,就会发现这一变量,如下所示:

 

    Thread.java  >>

1 public class Thread implements Runnable {
2     //这里省略了许多其他的代码
3     ThreadLocal.ThreadLocalMap threadLocals = null;
4 }

   ThreadLocalMap的定义是在ThreadLocal类中,真正的引用却是在Thread类中。

 

  这是JDK中Thread源码的一部分,从中我们可以看出ThreadLocalMap跟随着当前的线程而存在。不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量,这也是“副本”的含义。接下来我们再来看看ThreadLocal.ThreadLocalMap是如何定义的,以及ThreadLocal如何来操作它。如下所示:

 

  ThreadLocal.java  >>

复制代码
 1 public class ThreadLocal<T> {
 2     //这里省略了许多代码
 3 
 4     //将value的值保存于当前线程的本地变量中
 5     public void set(T value) {
 6         //获取当前线程
 7         Thread t = Thread.currentThread();
 8         //调用getMap方法获得当前线程中的本地变量ThreadLocalMap
 9         ThreadLocalMap map = getMap(t);
10         //如果TheadLocalMap已经存在,直接使用
11         if (map != null)
12             //以当前的ThreadLocal的实例为key,存储于当前线程的TheadLocalMap中,
13             //如果当前线程定义了多个不同的ThreadLocal的实例,则他们会作为不同key进行存储而不会互相干扰
15             map.set(this, value);
16         else
17             //如果TheadLocalMap不存在,则为当前创建一个新的
18             createMap(t, value);
19     }
20     //获取当前线程中以当前ThreadLocal实例为key的变量值
21     public T get() {
22         //获取当前线程
23         Thread t = Thread.currentThread();
24         //获取当前线程中的ThreadLocalMap
25         ThreadLocalMap map = getMap(t);
26         if (map != null) {
27             //获取当前线程中以当前ThreadLocal实例为key的变了值
28             ThreadLocalMap.Entry e = map.getEntry(this);
29             if (e != null)
30                 return (T)e.value;
31         }
32         //当map不存在时,设置初始值
33         return setInitialValue();
34     }
35     private T setInitialValue() {
36         T value = initialValue();
37         Thread t = Thread.currentThread();
38         ThreadLocalMap map = getMap(t);
39         if (map != null)
40             map.set(this, value);
41         else
42             createMap(t, value);
43         return value;
44     }   
45     //在当前线程中获取与之对应的TheadLocalMap 
46     ThreadLocalMap getMap(Thread t) {
47         return t.threadLocals;
48     }
49     //创建当前线程中的ThreadLocalMap
50     void createMap(Thread t, T firstValue) {
51         //调用构造函数生成当前线程中的TheadLocalMap
52         t.threadLocals = new ThreadLocalMap(this, firstValue);
53     }
54     //ThreadLocalMap的定义
55     static class ThreadLocalMap{
56         //这里省略了许多代码
57     }
58 }                
复制代码

 

  从上述代码中,我们看到了ThreadLocal类的大致结构和进行ThreadLocalMap的操作。我们可以从中得出以下结论:

  •   ThreadLocalMap变量属于线程的内部属性(是线程安全的),不同的线程拥有完全不同ThreadLocalMap变量。
  •   线程中的ThradLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的。
  •   在创建ThradLocalMap之前,会首先检查当前线程中的ThradLocalMap变量是否已经存在,如果不存在则创建一个;如果已经存在,则使用当前线程已经创建的ThradLocalMap。
  •   使用当前线程的ThradLocalMap的关键是在于使用当前的ThradLocal的实例作为key进行存储。

 

  ThradLocal模式至少从两个方面完成了数据访问隔离,即横向隔离和纵向隔离。有了横向和纵向两种不同的隔离方式,ThradLocal模式就能真正地做到线程安全。

  纵向隔离----线程与线程之间的数据访问隔离。这一点由线程的数据结构保证。因为每个线程在进行对象的访问时,访问的都是各个线程自己的ThradLocalMap。

  横向隔离----同一个线程中,不同的ThradLocal实例操作的对象之间相互隔离。这一点由ThradLocalMap在存储时采用当前ThradLocal的实例作为key来保证。

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

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:

synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

 

示例1:

这个类是Hibernate官方文档的HibernateUtil类,用于session管理。

 1 public class HibernateUtil {
 2     private static Log log = LogFactory.getLog(HibernateUtil.class);
 3     private static final SessionFactory sessionFactory;     //定义SessionFactory
 4  
 5     static {
 6         try {
 7             // 通过默认配置文件hibernate.cfg.xml创建SessionFactory
 8             sessionFactory = new Configuration().configure().buildSessionFactory();
 9         } catch (Throwable ex) {
10             log.error("初始化SessionFactory失败!", ex);
11             throw new ExceptionInInitializerError(ex);
12         }
13     }
14 
15     //创建线程局部变量session,用来保存Hibernate的Session
16     public static final ThreadLocal session = new ThreadLocal();
17  
18     /**
19      * 获取当前线程中的Session
20      * @return Session
21      * @throws HibernateException
22      */
23     public static Session currentSession() throws HibernateException {
24         Session s = (Session) session.get();
25         // 如果Session还没有打开,则新开一个Session
26         if (s == null) {
27             s = sessionFactory.openSession();
28             session.set(s);         //将新开的Session保存到线程局部变量中
29         }
30         return s;
31     }
32  
33     public static void closeSession() throws HibernateException {
34         //获取线程局部变量,并强制转换为Session类型
35         Session s = (Session) session.get();
36         session.set(null);
37         if (s != null)
38             s.close();
39     }
40 }

 

示例2:

创建一个Bean,通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:45:02
 * 学生
 */
public class Student {
    private int age = 0;   //年龄
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:53:33
 * 多线程下测试程序
 */
public class ThreadLocalDemo implements Runnable {
    //创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    public void run() {
        accessStudent();
    }
 
    /**
     * 示例业务方法,用来测试
     */
    public void accessStudent() {
        //获取当前线程的名字
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        //产生一个随机数并打印
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        //获取一个Student对象,并将随机数年龄插入到对象属性中
        Student student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //获取本地线程变量并强制转换为Student类型
        Student student = (Student) studentLocal.get();
        //线程首次执行此方法的时候,studentLocal.get()肯定为null
        if (student == null) {
            //创建一个Student对象,并保存到本地线程变量studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}

运行结果:

a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27

 

参考:http://www.cnblogs.com/gw811/archive/2012/09/07/2675105.html

     http://www.iteye.com/topic/103804

     http://lavasoft.blog.51cto.com/62575/51926/

 

 

posted @ 2014-04-03 13:06  sky钦  阅读(104)  评论(0)    收藏  举报