Struts2中的设计模式----ThreadLocal模式
http://www.cnblogs.com/gw811/archive/2012/09/07/2675105.html
设计模式(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进行存储。
ThreadLocal模式的应用场景
在分析了ThreadLocal的源码之后,我们来看看ThreadLocal模式最合适的业务场景。在一个完整的“请求-响应”过程中,主线程的执行过程总是贯穿始终。当这个主线程的执行过程中被加入了ThreadLocal的读写时,会对整个过程产生怎样的影响呢?我们根据之前源码分析的结果,并结合 分层开发模式,把整个流程画下来,如图4-1所示:
从上面图中我们可以看到,由于ThreadLocal所操作的是维持于整个Thread生命周期的副本(ThreadLocalMap),所以无论在 J2EE程序的哪个层次(表示层、业务逻辑层或者持久层),只要在一个Thread的生命周期之内,存储于ThreadLocalMap中的对象都是 线程安全的(因为ThreadLocalMap本身仅仅隶属于当前的执行线程,是执行线程内部的一个属性变量。我们用图中的阴影部分来表示这个变量的存储空间)。而这一点,正是被我们用于来解决多线程环境中的变量共享问题的核心技术。ThreadLocal的这一特性也使其能够被广泛地应用于J2EE开发 中的许多业务场景。
数据共享,还是数据传递
ThreadLocal模式由于利用了Java自身的语法特性而显得异常简单和方便,因而被广泛使用于J2EE开发,尤其是应对跨层次的资源共享,例如在Spring中,就有使用ThreadLocal模式来管理数据库连接或者Hibernate的Session的范例。
在一些比较著名的论坛中,有很多关于使用ThreadLocal模式来做数据传递的讨论。事实上,这是对ThreadLocal模式的一个极大的误解。需要注意的是,ThreadLocal模式解决的是同一线程中隶属于不同开发层次的数据共享问题,而不是在不同的开发层次中进行数据传递。
1)ThreadLocal模式的核心在于实现一个共享环境(类的内部封装了ThreadLocal的静态实例)。所以,在操作ThreadLocal时,这一共享环境会跨越多个开发层次而随处存在。
2)随处存在的共享环境造成了所有的开发层次的共同依赖,从而使得所有的开发层次都耦合在一起,从而变得无法独立测试。
3)数据传递应该通过接口函数的签名显式声明,这样才能够从接口声明中表达接口所表达的真正含义。ThreadLocal模式位于实现的内部,从而使得接口与接口之间无法达成一致的声明默契。
Struts2的解耦合的设计理念使得Struts2的MVC实现成为了使用ThreadLocal模式的天然场所。在第三章中,我们已经介绍了一些基本 概念,Struts2通过引入XWork框架,将整个Http请求的过程拆分成为与Web容器有关和与Web容器无关的两个执行阶段。而这两个阶段的数据 交互就是通过ThreadLocal模式中的线程共享副本安全地进行。在其中,我们没有看到数据传递,存在的只是整个执行线程的数据共享。
ThreadLocal模式的核心元素
仔细分析上一节的示意图(图4-1),我们可以发现,要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例(也就是执行示意图中的菱形部分)。而这一点,我们可以通过类的静态实例变量来实现,这个用于承载静态实例变量的类就被视作是一个共享环境。我们来看一个例子,如代码清单如下所示:
1 public class Counter { 2 //新建一个静态的ThreadLocal变量,并通过get方法将其变为一个可访问的对象 3 private static ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>(){ 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 }
在这个Counter类中,我们实现了一个静态的ThreadLocal变量,并通过get方法将ThreadLocal中存储的值暴露出来。我们还封装了一个带有业务逻辑的方法getNextCounter,操作ThreadLocal中的值,将其加1,并返回计算后的值。
此时,Counter类就变成了一个数据共享环境,我们也拥有了实现ThreadLocal模式的关键要素。有了它,我们来编写一个简单的测试,如代码清单如下所示:
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 }
这是一个简单的线程类,循环输出当前线程的名称和getNextCounter的结果,由于getNextCounter中的逻辑所操作的是 ThreadLocal中的变量,所以无论同时有多少个线程在运行,返回的值将仅与当前线程的变量值有关,也就是说,在同一个线程中,变量值会被连续累 加。这一点可以通过如下的测试代码证实:
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事件处理体系设计的核心。
----完----
ThradLocal模式至少从两个方面完成了数据访问隔离,即横向隔离和纵向隔离。有了横向和纵向两种不同的隔离方式,ThradLocal模式就能真正地做到线程安全。
纵向隔离----线程与线程之间的数据访问隔离。这一点由线程的数据结构保证。因为每个线程在进行对象的访问时,访问的都是各个线程自己的ThradLocalMap。
横向隔离----同一个线程中,不同的ThradLocal实例操作的对象之间相互隔离。这一点由ThradLocalMap在存储时采用当前ThradLocal的实例作为key来保证。
深入比较TheadLocal模式与synchronized关键字
ThreadLocal模式synchronized关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。
1)ThreadLocal是一个java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
同步机制(synchronized关键字)采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
posted on 2013-11-22 16:16 heartstage 阅读(292) 评论(0) 编辑 收藏 举报