本文主要整理自----《Struts2技术内幕》 陆舟 著
设计模式(Design pattern):是经过程序员反复实践后形成的一套代码设计经验的总结。
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对象并不是一个线程安全的对象。
为了篇幅问题,也为了尊重zwchen的原著,Servlet对象并不是一个线程安全的对象的测试实例单独写了一篇文章:
如果你想很好的了解这篇文章,最好读一下!
有关线程安全的概念范畴
谈到线程安全,去多初学者很容易在概念上混淆。线程安全,指的是在多线程环境下,一个类在执行某个方法时,对类的内部实例变量的访问时安全的。因此,对于下面列出来的2类变量,不存在任何线程安全的说法:
1)方法签名中的任何参数变量。
2)处于方法内部的局部变量。
任何针对上述形式的变量的访问都是线程安全的,因为他们都是处于方法的内部,由当前的执行线程独自管理。
这就是线程安全问题的由来:在传统的基于Servlet的开发模式中,Servlet对象内部的实例变量不是线程安全。在多线程环境中,这些变量的访问需要通过特殊的手段进行访问控制。
解决线程安全访问的方法很多,比较容易想到的一种方案是使用同步机制(Synchronized),但是出于对Web应用效率的考虑,这种机制在Web开发中的可行性很低,也违背了Servlet的设计初衷。因此,我们需要另辟蹊径来解决这一困扰我们的问题。
ThreadLocal模式的实现机理
在JDK的早期版本中,提供了一种解决多线程并发问题的方案:java.lang.ThreadLocal类。ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。
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模式synchronized关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。
1)ThreadLocal是一个java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
同步机制(synchronized关键字)采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
未完!待续!!!
Struts2中的设计模式----ThreadLocal模式续