- 浏览: 716923 次
- 性别:
- 来自: 上海
最新评论
-
A741841403:
附件怎么看不到了呢?
介绍一个PDF的生成方案 -
chunguang_yao:
写得非常好
《Struts2技术内幕》 新书部分篇章连载(一)—— 如何学习开源框架 -
su_chunlong:
rrredriver 写道极赞的文章,感谢楼主!有一个问题想请 ...
SpringMVC深度探险(三) —— DispatcherServlet与初始化主线 -
qq_32671287:
...
SpringMVC深度探险(一) —— SpringMVC前传 -
shenghuorulan:
看的好爽,可不可以这么笼统的概括一个框架的学习过程1,框架要解 ...
SpringMVC深度探险(二) —— SpringMVC概览
第4章 源头活水 —— Struts2中的设计模式
设计模式(Design pattern)是经过程序员反复实践后形成的一套代码设计经验的总结。设计模式随着编程语言的发展,也由最初的“编程惯例”逐步发展成为被反复使用、并为绝大多数程序员所知晓的、完善的理论体系。我们使用设计模式(Design pattern)的初衷,是使代码的重用度提高、让代码能够更容易被别人理解以及保证代码的可靠性。毫无疑问,在程序中使用设计模式无论是对于程序员自身还是对于应用程序都是双赢的结果。正确地使用设计模式,能够使我们编程真正实现工程化和规范化,并且在一定程度上指导着框架的设计和实现。
在深入探讨Struts2所依赖的核心技术之前,我们将首先带领读者领略一下在整个Struts2框架之中所使用到的一些最常用的设计模式。理解这些设计模式的运用场景和内部机理,也将为日后我们对这些核心技术的分析打下坚实的基础。
4.1 ThreadLocal模式
ThreadLocal模式,严格意义上来说并不能称之为一种设计模式,因为它只是一个用来解决多线程程序中数据共享问题的一个解决方案。尽管如此,ThreadLocal模式却贯穿了整个Struts2和XWork框架,成为Struts2框架进行“解耦”设计的核心依赖技术。那么,为什么要在Struts2中引入ThreadLocal模式呢?这不得不从Web开发中的线程安全问题谈起。
4.1.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对象并不是一个线程安全的对象。下面的测试代码将证实这一点:
这里参阅了网络上一段著名的对Servlet线程安全性进行测试的代码(http://zwchen.iteye.com/blog/91088)。运行之后,我们可以看一下这个例子的输出:
通过上面的输出,我们可以得出以下三个Servlet对象的运行特性:
1. Servlet对象是一个无状态的单例对象(Singleton),因为我们看到多次请求的this指针所打印出来的hashcode值都相同
2. Servlet在不同的线程(线程池)中运行,如http-8081-Processor22和http-8081-Processor23等输出值可以明显区分出不同的线程执行了同一段Servlet逻辑代码。
3. Counter变量在不同的线程中共享,而且它的值被不同的线程修改,输出时已经不是顺序输出。也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的。
【有关线程安全的概念范畴】
谈到线程安全,对于许多初学者来说很容易引起概念上的混淆。线程安全,指的是在多线程环境下,一个类在执行某个方法时,对类的内部实例变量的访问安全与否。因此,对于下面列出来的2类变量,不存在任何线程安全的说法:
1)方法签名中的任何参数变量。
2)处于方法内部的局部变量。
任何针对上述形式的变量的访问都是线程安全的,因为它们都处于方法体的内部,由当前的执行线程独自管理。
这就是线程安全问题的由来:在传统的基于Servlet的开发模式中,Servlet对象内部的实例变量不是线程安全的。在多线程环境中,这些变量的访问需要通过特殊的手段进行访问控制。
解决线程安全访问的方法很多,比较容易想到的一种方案是使用同步机制,但是出于对Web应用效率的考虑,这种机制在Web开发中的可行性很低,也违背了Servlet的设计初衷。因此,我们需要另辟蹊径来解决这一困扰我们的问题。
4.1.2 ThreadLocal模式的实现机理
在JDK的早期版本中,提供了一种解决多线程并发问题的方案: java.lang.ThreadLocal类。ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。
ThreadLocal本身并不是一个线程,而是通过操作当前线程(Thread)中的一个内部变量来达到与其他线程隔离的目的。之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程(Thread)的一个本地变量。如果我们看一下Thread的源码实现,就会发现这一变量,如代码清单4-2所示:
这是JDK中Thread源码的一部分,从中我们可以看出ThreadLocalMap跟随着当前的线程而存在。不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量,这也就是“副本”的含义。接下来我们再来看看ThreadLocal.ThreadLocalMap是如何定义的,以及ThreadLocal如何来操作它,如代码清单4-3所示:
从上述代码中,我们看到了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来保证。
ThreadLocal模式并不是什么高深的学问,它甚至从JDK1.2开始就存在于Java世界中。由此可见,我们掌握一种知识的最终目的是熟练而合理地运用它。
【深入比较ThreadLocal模式与synchronized关键字】
ThreadLocal模式与synchronized关键字都是用于处理多线程并发访问变量的问题。只是两者处理问题的角度和思路不同。
1)ThreadLocal是一个Java类,通过对当前线程(Thread)中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程(Thread)都拥有其副本。
2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量在访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量是多个线程共享的。
同步机制采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不影响。
4.1.3 ThreadLocal模式的应用场景
在分析了ThreadLocal的源码之后,我们来看看ThreadLocal模式最合适的业务场景。在一个完整的“请求-响应”过程中,主线程的执行过程总是贯穿始终。当这个主线程的执行过程中被加入了ThreadLocal的读写时,会对整个过程产生怎样的影响呢?我们根据之前源码分析的结果,并结合分层开发模式,把整个流程画下来,如图4-1所示:
从上面图中我们可以看到,由于ThreadLocal所操作的是维持于整个Thread生命周期的副本(ThreadLocalMap),所以无论在J2EE程序程序的哪个层次(表示层、业务逻辑层或者持久层),只要在一个Thread的生命周期之内,存储于ThreadLocalMap中的对象都是线程安全的(因为ThreadLocalMap本身仅仅隶属于当前的执行线程,是执行线程内部的一个属性变量。我们用图中的阴影部分来表示这个变量的存储空间)。而这一点,正是被我们用于来解决多线程环境中的变量共享问题的核心技术。ThreadLocal的这一特性也使其能够被广泛地应用于J2EE开发中的许多业务场景。
【数据共享 OR 数据传递?】
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模式中的线程共享副本安全地进行。在其中,我们没有看到数据传递,存在的只是整个执行线程的数据共享。
4.1.4 ThreadLocal模式的核心元素
仔细分析上一节的示意图(图4-1),我们可以发现,要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例(也就是执行示意图中的菱形部分)。而这一点,我们可以通过类的静态实例变量来实现,这个用于承载静态实例变量的类就被视作是一个共享环境。我们来看一个例子,如代码清单4-4所示:
在这个Counter类中,我们实现了一个静态的ThreadLocal变量,并通过get方法将ThreadLocal中存储的值暴露出来。我们还封装了一个带有业务逻辑的方法getNextCounter,操作ThreadLocal中的值,将其加1,并返回计算后的值。
此时,Counter类就变成了一个数据共享环境,我们也拥有了实现ThreadLocal模式的关键要素。有了它,我们来编写一个简单的测试,如代码清单4-5所示:
这是一个简单的线程类,循环输出当前线程的名称和getNextCounter的结果,由于getNextCounter中的逻辑所操作的是ThreadLocal中的变量,所以无论同时有多少个线程在运行,返回的值将仅与当前线程的变量值有关,也就是说,在同一个线程中,变量值会被连续累加。这一点可以通过如下的测试代码证实:
我们来运行一下上面的代码,并看看输出结果:
上面的输出结果也证实了,counter的值在多线程环境中的访问是线程安全的。从对例子的分析中我们可以再次体会到,ThreadLocal模式最合适的使用场景:在同一个线程(Thread)的不同开发层次中共享数据。
从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
建立在ThreadLocal模式的实现步骤之上,ThreadLocal的使用则更加简单。在线程执行的任何地方,我们都可以通过访问共享数据类中所提供的ThreadLocal变量的设值和取值方法安全地获得当前线程中安全的变量值。
这两个步骤,我们之后会在Struts2的实现中多次提及,读者只要能充分理解ThreadLocal处理多线程访问的基本原理,就能对Struts2的数据访问和数据共享的设计有一个整体的认识。
讲到这里,我们回过头来看看ThreadLocal模式的引入,到底对我们的编程模型有什么重要的意义呢?
这一点,是由ThreadLocal模式的实现机理决定的。因为实现ThreadLocal模式的一个重要步骤,就是构建一个静态的共享存储空间。从而使得任何对象在任何时刻都可以安全地对数据进行访问。
这一点是ThreadLocal模式给我们带来的最为核心的一个影响。因为在一般情况下,Java对象之间的协作关系,主要通过参数和返回值来进行消息传递,这也是对象协作之间的一个重要依赖。而ThreadLocal模式彻底打破了这种依赖关系,通过线程安全的共享对象来进行数据共享,可以有效避免在编程层次之间形成数据依赖。这也成为了XWork事件处理体系设计的核心。
恩,只要满足共享的数据环境即可。
我们这里讨论的Web开发框架与异步开发框架的设计理念是不同的。ThreadLocal只是一个常见的Java技术,不用把它放到框架的层面去考虑。
其实我在这里说得非常清楚,数据传递和数据共享是完全不同的概念。ThreadLocal只能解决不同编程层次的数据共享问题而不能代替数据传递。
你这个问题大约在05年的时候在javaeye上有很激烈的讨论,正反各执一词。我的观点是,Page信息应视作参数进行传递,而非在所有层次进行共享。这里有2个方面的原因:
1. 将page信息作为参数进行传递能够使得业务逻辑层的调用接口的契约更加明确。
从外界来看,如果page信息不在你的接口定义之中,调用者将无法知晓你的这个接口到底在做一个什么样的事情。
2. 将page信息置于ThreadLocal中,并在其他的编程层次读取,使得每一个编程层次都严格绑定在了当前线程之中,这样一来你每一个层次的程序都无法单独进行单元测试。
这一点就非常致命了,你可以在实际编程中仔细想一想。
设计模式(Design pattern)是经过程序员反复实践后形成的一套代码设计经验的总结。设计模式随着编程语言的发展,也由最初的“编程惯例”逐步发展成为被反复使用、并为绝大多数程序员所知晓的、完善的理论体系。我们使用设计模式(Design pattern)的初衷,是使代码的重用度提高、让代码能够更容易被别人理解以及保证代码的可靠性。毫无疑问,在程序中使用设计模式无论是对于程序员自身还是对于应用程序都是双赢的结果。正确地使用设计模式,能够使我们编程真正实现工程化和规范化,并且在一定程度上指导着框架的设计和实现。
在深入探讨Struts2所依赖的核心技术之前,我们将首先带领读者领略一下在整个Struts2框架之中所使用到的一些最常用的设计模式。理解这些设计模式的运用场景和内部机理,也将为日后我们对这些核心技术的分析打下坚实的基础。
4.1 ThreadLocal模式
ThreadLocal模式,严格意义上来说并不能称之为一种设计模式,因为它只是一个用来解决多线程程序中数据共享问题的一个解决方案。尽管如此,ThreadLocal模式却贯穿了整个Struts2和XWork框架,成为Struts2框架进行“解耦”设计的核心依赖技术。那么,为什么要在Struts2中引入ThreadLocal模式呢?这不得不从Web开发中的线程安全问题谈起。
4.1.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对象并不是一个线程安全的对象。下面的测试代码将证实这一点:
public class ThreadSafeTestServlet extends HttpServlet { // 定义一个实例变量,并非一个线程安全的变量 private int counter = 0; public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 输出当前Servlet的信息以及当前线程的信息 System.out.println(this + ":" + Thread.currentThread()); // 循环,并增加实例变量counter的值 for (int i = 0; i < 5; i++) { System.out.println("Counter = " + counter); try { Thread.sleep((long) Math.random() * 1000); counter++; } catch (InterruptedException exc) { } } } }
这里参阅了网络上一段著名的对Servlet线程安全性进行测试的代码(http://zwchen.iteye.com/blog/91088)。运行之后,我们可以看一下这个例子的输出:
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变量在不同的线程中共享,而且它的值被不同的线程修改,输出时已经不是顺序输出。也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的。
【有关线程安全的概念范畴】
谈到线程安全,对于许多初学者来说很容易引起概念上的混淆。线程安全,指的是在多线程环境下,一个类在执行某个方法时,对类的内部实例变量的访问安全与否。因此,对于下面列出来的2类变量,不存在任何线程安全的说法:
1)方法签名中的任何参数变量。
2)处于方法内部的局部变量。
任何针对上述形式的变量的访问都是线程安全的,因为它们都处于方法体的内部,由当前的执行线程独自管理。
这就是线程安全问题的由来:在传统的基于Servlet的开发模式中,Servlet对象内部的实例变量不是线程安全的。在多线程环境中,这些变量的访问需要通过特殊的手段进行访问控制。
解决线程安全访问的方法很多,比较容易想到的一种方案是使用同步机制,但是出于对Web应用效率的考虑,这种机制在Web开发中的可行性很低,也违背了Servlet的设计初衷。因此,我们需要另辟蹊径来解决这一困扰我们的问题。
4.1.2 ThreadLocal模式的实现机理
在JDK的早期版本中,提供了一种解决多线程并发问题的方案: java.lang.ThreadLocal类。ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。
ThreadLocal本身并不是一个线程,而是通过操作当前线程(Thread)中的一个内部变量来达到与其他线程隔离的目的。之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程(Thread)的一个本地变量。如果我们看一下Thread的源码实现,就会发现这一变量,如代码清单4-2所示:
public class Thread implements Runnable { // 这里省略了许多其他的代码 ThreadLocal.ThreadLocalMap threadLocals = null; }
这是JDK中Thread源码的一部分,从中我们可以看出ThreadLocalMap跟随着当前的线程而存在。不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量,这也就是“副本”的含义。接下来我们再来看看ThreadLocal.ThreadLocalMap是如何定义的,以及ThreadLocal如何来操作它,如代码清单4-3所示:
public class ThreadLocal<T> { // 这里省略了许多其他代码 // 将value的值保存于当前线程的本地变量中 public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 调用getMap方法获得当前线程中的本地变量ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果ThreadLocalMap已存在,直接使用 if (map != null) // 以当前的ThreadLocal的实例作为key,存储于当前线程的 // ThreadLocalMap中,如果当前线程中被定义了多个不同的ThreadLocal // 的实例,则它们会作为不同key进行存储而不会互相干扰 map.set(this, value); else // ThreadLocalMap不存在,则为当前线程创建一个新的 createMap(t, value); } // 获取当前线程中以当前ThreadLocal实例为key的变量值 public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程中的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 获取当前线程中以当前ThreadLocal实例为key的变量值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } // 当map不存在时,设置初始值 return setInitialValue(); } // 从当前线程中获取与之对应的ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 创建当前线程中的ThreadLocalMap void createMap(Thread t, T firstValue) { // 调用构造函数生成当前线程中的ThreadLocalMap t.threadLocals = new ThreadLocalMap(this, firstValue); } // ThreadLoaclMap的定义 static class ThreadLocalMap { // 这里省略了许多代码 } }
从上述代码中,我们看到了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来保证。
ThreadLocal模式并不是什么高深的学问,它甚至从JDK1.2开始就存在于Java世界中。由此可见,我们掌握一种知识的最终目的是熟练而合理地运用它。
【深入比较ThreadLocal模式与synchronized关键字】
ThreadLocal模式与synchronized关键字都是用于处理多线程并发访问变量的问题。只是两者处理问题的角度和思路不同。
1)ThreadLocal是一个Java类,通过对当前线程(Thread)中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程(Thread)都拥有其副本。
2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量在访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量是多个线程共享的。
同步机制采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不影响。
4.1.3 ThreadLocal模式的应用场景
在分析了ThreadLocal的源码之后,我们来看看ThreadLocal模式最合适的业务场景。在一个完整的“请求-响应”过程中,主线程的执行过程总是贯穿始终。当这个主线程的执行过程中被加入了ThreadLocal的读写时,会对整个过程产生怎样的影响呢?我们根据之前源码分析的结果,并结合分层开发模式,把整个流程画下来,如图4-1所示:
从上面图中我们可以看到,由于ThreadLocal所操作的是维持于整个Thread生命周期的副本(ThreadLocalMap),所以无论在J2EE程序程序的哪个层次(表示层、业务逻辑层或者持久层),只要在一个Thread的生命周期之内,存储于ThreadLocalMap中的对象都是线程安全的(因为ThreadLocalMap本身仅仅隶属于当前的执行线程,是执行线程内部的一个属性变量。我们用图中的阴影部分来表示这个变量的存储空间)。而这一点,正是被我们用于来解决多线程环境中的变量共享问题的核心技术。ThreadLocal的这一特性也使其能够被广泛地应用于J2EE开发中的许多业务场景。
【数据共享 OR 数据传递?】
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模式中的线程共享副本安全地进行。在其中,我们没有看到数据传递,存在的只是整个执行线程的数据共享。
4.1.4 ThreadLocal模式的核心元素
仔细分析上一节的示意图(图4-1),我们可以发现,要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例(也就是执行示意图中的菱形部分)。而这一点,我们可以通过类的静态实例变量来实现,这个用于承载静态实例变量的类就被视作是一个共享环境。我们来看一个例子,如代码清单4-4所示:
public class Counter { // 新建一个静态的ThreadLocal变量,并通过get方法将其变为一个可访问的对象 private static ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>() { protected synchronized Integer initialValue() { return 10; } }; // 通过静态的get方法访问ThreadLocal中存储的值 public static Integer get() { return counterContext.get(); } // 通过静态的set方法将变量值设置到ThreadLocal中 public static void set(Integer value) { counterContext.set(value); } // 封装业务逻辑,操作存储于ThreadLocal中的变量 public static Integer getNextCounter() { counterContext.set(counterContext.get() + 1); return counterContext.get(); } }
在这个Counter类中,我们实现了一个静态的ThreadLocal变量,并通过get方法将ThreadLocal中存储的值暴露出来。我们还封装了一个带有业务逻辑的方法getNextCounter,操作ThreadLocal中的值,将其加1,并返回计算后的值。
此时,Counter类就变成了一个数据共享环境,我们也拥有了实现ThreadLocal模式的关键要素。有了它,我们来编写一个简单的测试,如代码清单4-5所示:
public class ThreadLocalTest extends Thread { public void run() { for(int i = 0; i < 3; i++){ System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter()); } } }
这是一个简单的线程类,循环输出当前线程的名称和getNextCounter的结果,由于getNextCounter中的逻辑所操作的是ThreadLocal中的变量,所以无论同时有多少个线程在运行,返回的值将仅与当前线程的变量值有关,也就是说,在同一个线程中,变量值会被连续累加。这一点可以通过如下的测试代码证实:
public class Test { public static void main(String[] args) throws Exception { ThreadLocalTest testThread1 = new ThreadLocalTest(); ThreadLocalTest testThread2 = new ThreadLocalTest(); ThreadLocalTest testThread3 = new ThreadLocalTest(); testThread1.start(); testThread2.start(); testThread3.start(); } }
我们来运行一下上面的代码,并看看输出结果:
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模式的引入,到底对我们的编程模型有什么重要的意义呢?
downpour 写道
结论 使用ThreadLocal模式,可以使得数据在不同的编程层次得到有效地共享。
这一点,是由ThreadLocal模式的实现机理决定的。因为实现ThreadLocal模式的一个重要步骤,就是构建一个静态的共享存储空间。从而使得任何对象在任何时刻都可以安全地对数据进行访问。
downpour 写道
结论 使用ThreadLocal模式,可以对执行逻辑与执行数据进行有效解耦。
这一点是ThreadLocal模式给我们带来的最为核心的一个影响。因为在一般情况下,Java对象之间的协作关系,主要通过参数和返回值来进行消息传递,这也是对象协作之间的一个重要依赖。而ThreadLocal模式彻底打破了这种依赖关系,通过线程安全的共享对象来进行数据共享,可以有效避免在编程层次之间形成数据依赖。这也成为了XWork事件处理体系设计的核心。
评论
12 楼
liguanqun811
2013-09-16
对struts2-core-2.3.8中,StrutsPrepareAndExecuteFilter 中的doFilter中的prepare.assignDispatcherToThread()不理解,请问将dispatcher绑定到线程就能实现对dispatcher的操作线程安全了吗?我感觉dispatcher对象始终只有一个啊,并没有哪个地方拷贝副本啊
11 楼
fj359941160
2013-03-31
看来看去,还是楼主这篇关于ThreadLocal的讲的最清晰。
呵呵,学习了。
呵呵,学习了。
10 楼
chaugust02
2012-11-13
9 楼
潴潴爱喵喵
2012-11-06
楼主好:
1)ThreadLocal模式的核心在于实现一个共享环境(类的内部封装了ThreadLocal的静态实例)。所以,在操作ThreadLocal时,这一共享环境会跨越多个开发层次而随处存在。
2)随处存在的共享环境造成了所有的开发层次的共同依赖,从而使得所有的开发层次都耦合在了一起,从而变得无法独立测试。
3)数据传递应该通过接口函数的签名显式声明,这样才能够从接口声明中表达接口所表达的真正含义。ThreadLocal模式位于实现的内部,从而使得接口与接口之间无法达成一致的声明契约。
这里说的第二点我有点迷糊。ThreadLocal创建了共享环境,所以造成了开发层次之间耦合在一起了。而xwork处理过程和strust2预处理过程应该是解耦的啊?难道这两个过程之间还是有一定耦合的部分,就是threadlocal共享的数据部分么?
1)ThreadLocal模式的核心在于实现一个共享环境(类的内部封装了ThreadLocal的静态实例)。所以,在操作ThreadLocal时,这一共享环境会跨越多个开发层次而随处存在。
2)随处存在的共享环境造成了所有的开发层次的共同依赖,从而使得所有的开发层次都耦合在了一起,从而变得无法独立测试。
3)数据传递应该通过接口函数的签名显式声明,这样才能够从接口声明中表达接口所表达的真正含义。ThreadLocal模式位于实现的内部,从而使得接口与接口之间无法达成一致的声明契约。
这里说的第二点我有点迷糊。ThreadLocal创建了共享环境,所以造成了开发层次之间耦合在一起了。而xwork处理过程和strust2预处理过程应该是解耦的啊?难道这两个过程之间还是有一定耦合的部分,就是threadlocal共享的数据部分么?
8 楼
873339698
2012-03-01
对threadlocal有了更深的理解了.
博文中一点点小瑕疵,作者勿怪
菱形部分--->平行四边形
博文中一点点小瑕疵,作者勿怪
菱形部分--->平行四边形
7 楼
downpour
2012-02-15
aris2012 写道
从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
恩,只要满足共享的数据环境即可。
6 楼
aris2012
2012-02-15
从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
5 楼
Rhain
2012-01-09
多谢指点!顺便问下为什么你的struts2技术内幕还是在预定中,咋还不能购买呀?
4 楼
downpour
2012-01-09
bill2004158 写道
使用ThreadLocal模式的前提是处理各层函数调用的必须是同一个线程?
也就是现在流行的异步处理就不适用了?
也就是现在流行的异步处理就不适用了?
我们这里讨论的Web开发框架与异步开发框架的设计理念是不同的。ThreadLocal只是一个常见的Java技术,不用把它放到框架的层面去考虑。
3 楼
bill2004158
2012-01-08
使用ThreadLocal模式的前提是处理各层函数调用的必须是同一个线程?
也就是现在流行的异步处理就不适用了?
也就是现在流行的异步处理就不适用了?
2 楼
downpour
2012-01-07
Rhain 写道
请问ThreadLocal是否可以用来处理分页数据的共享如:“当前页,页面显示的个数”,每次的请求都会附带这两个参数,在控制层就通过ThreadLoacl来获取这两个参数的值?
其实我在这里说得非常清楚,数据传递和数据共享是完全不同的概念。ThreadLocal只能解决不同编程层次的数据共享问题而不能代替数据传递。
你这个问题大约在05年的时候在javaeye上有很激烈的讨论,正反各执一词。我的观点是,Page信息应视作参数进行传递,而非在所有层次进行共享。这里有2个方面的原因:
1. 将page信息作为参数进行传递能够使得业务逻辑层的调用接口的契约更加明确。
从外界来看,如果page信息不在你的接口定义之中,调用者将无法知晓你的这个接口到底在做一个什么样的事情。
2. 将page信息置于ThreadLocal中,并在其他的编程层次读取,使得每一个编程层次都严格绑定在了当前线程之中,这样一来你每一个层次的程序都无法单独进行单元测试。
这一点就非常致命了,你可以在实际编程中仔细想一想。
1 楼
Rhain
2012-01-07
请问ThreadLocal是否可以用来处理分页数据的共享如:“当前页,页面显示的个数”,每次的请求都会附带这两个参数,在控制层就通过ThreadLoacl来获取这两个参数的值?
发表评论
-
《Struts2技术内幕》 新书部分篇章连载(十)—— XWork概览
2012-01-29 14:43 7416第7章 别具匠心 —— XWo ... -
《Struts2技术内幕》 新书部分篇章连载(九)—— 强大的OGNL
2012-01-29 13:17 5106第6章 灵丹妙药 —— OGN ... -
《Struts2技术内幕》 新书部分篇章连载(八)—— XWork容器概览
2012-01-29 11:56 4716第5章 生命之源 —— XWork中的容器 对象的生命周期管 ... -
Struts2的一些不尽人意的地方,兼答hantsy
2012-01-06 10:21 4777hantsy 写道 在 Webwork 合并到Apache S ... -
《Struts2技术内幕》 新书部分篇章连载(六)—— 框架的本质
2012-01-05 14:02 4850第2章 固本清源 —— Web ... -
《Struts2技术内幕》自评 —— 尚未完成的话题
2011-12-30 11:11 4206此文接我另外一篇博客:新书上市:《Struts2技术内幕》 ... -
新书上市:《Struts2技术内幕》
2011-12-26 14:28 10557我的新书《Struts2技术内 ... -
《Struts2技术内幕》 新书部分篇章连载(四)—— 核心分发器
2011-10-27 20:15 75429.2核心分发器 —— Dispa ... -
《Struts2技术内幕》 新书部分篇章连载(五)—— 请求响应哲学
2011-10-27 20:01 14050第7章 别具匠心 —— XWork设计原理 众所周知,现代电 ... -
《Struts2技术内幕》 新书部分篇章连载(三)—— 多视角透析Struts2
2011-10-27 19:09 88353.3 多视角透析Struts2 Str ... -
《Struts2技术内幕》 新书部分篇章连载(一)—— 如何学习开源框架
2011-10-27 18:40 161942.6 如何学习开源框架 ... -
《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
2011-10-26 19:46 9729第2章 固本清源 —— Web ... -
《Struts2技术内幕》 新书样章和导读
2011-10-27 20:40 5070由于本书尚未出版,我 ... -
介绍一个PDF的生成方案
2009-11-03 17:15 37506在Java世界,要想生成PDF ... -
忘记李刚,一步一步跟我学Struts2 —— 标签库,永恒的争论话题
2009-02-08 22:52 7663专栏地址:http://www.iteye ... -
忘记李刚,一步一步跟我学Struts2 —— Result机制,让视图更丰富
2009-02-04 23:56 12655专栏地址:http://www.iteye.com/wiki/ ... -
忘记李刚,一步一步跟我学Struts2 —— 拦截器详解
2009-02-01 12:49 10852专栏地址:http://www.iteye.com/wiki/ ... -
Spring Security 2 配置精讲
2009-01-22 15:19 29434论坛上看了不少Spring Sec ... -
忘记李刚,一步一步跟我学Struts2 —— MVC框架的困惑
2009-01-21 11:43 10219专栏地址:http://www.iteye.com/wiki/ ... -
忘记李刚,一步一步跟我学Struts2 —— Struts2配置详解
2009-01-19 10:06 6350专栏地址:http://www.iteye.com/wiki/ ...
相关推荐
总结一下,这份资料涵盖了设计模式中的单例模式、工厂模式和代理模式,以及Java中的ThreadLocal特性。理解并熟练应用这些概念和技术,对于提升Java开发者的技能水平,优化代码的可读性和可维护性具有重要作用。在...
本教程“Struts2 学习 2”将深入探讨Struts2的核心概念和技术,通过一系列文档帮助你掌握这个框架的关键要点。 一、ActionContext的理解与应用 在“第九讲 ActionContext.docx”中,你将学习到ActionContext是...
Struts2是一个非常流行的Java Web框架,用于构建可维护性和可扩展性良好的企业级应用程序。在Struts2中,拦截器扮演着至关重要的角色,它们是实现业务逻辑和控制流程的核心组件。本知识点主要聚焦于如何利用Struts2...
### ThreadLocal核心概念详解 #### 一、ThreadLocal简介与重要性 ThreadLocal是一个非常重要的Java并发工具类,它的核心概念在于为每一个线程提供了一个独立的变量副本,从而避免了线程之间的数据竞争问题。这使得...
【ThreadLocal模式管理Session的理解】 在使用Hibernate进行数据库操作时,正确管理Session是优化系统性能的关键。Session由SessionFactory创建,而SessionFactory是线程安全的,这意味着它可以被多个并发线程共享...
**线程局部变量(ThreadLocal)是Java编程中一个非常重要的工具类,它在多线程环境下提供了线程安全的数据存储。ThreadLocal并不是一个变量,而是一个类,它为每个线程都创建了一个独立的变量副本,使得每个线程都...
2. **事务处理**:在事务管理中,`ThreadLocal`可以用于维护每个线程的事务状态,确保事务的完整性和一致性。 3. **日志记录**:在多线程环境中,`ThreadLocal`可以用于维护每个线程的日志上下文,如线程ID、用户名...
象,如HttpServletRequest、HttpServletResponse,或者需要访问Session和Application等信息,在Struts2框架中,我们可以利用ActionContext和ServletActionContext这两个类。ActionContext是Struts2提供的一个核心上...
同时, ThreadLocal 还提供了扩容机制,当数组的 size 大于总长度的 2/3 时,会触发扩容操作,扩容后将原来的数组长度乘以 2倍。 在 ThreadLocal 中,set 方法的逻辑是先获取当前线程的存取副本变量的 map,然后...
理解ThreadLocal 理解ThreadLocal 理解ThreadLocal 理解ThreadLocal
在Struts2框架中,ActionContext充当了Action的上下文环境的角色,它保存了request、session、parameters、locale等信息。通过ThreadLocal,可以保证这些信息在Action的不同调用中保持一致,即在同一个请求的不同...
Java中的ThreadLocal是一个非常重要的工具类,它在多线程编程中扮演着独特角色,尤其在处理线程间数据隔离和共享时。ThreadLocal不是线程本身,而是为每个线程提供一个独立的变量副本,使得每个线程都可以独立地改变...
在 `LeakingServlet` 的 `doGet` 方法中,如果 `ThreadLocal` 没有设置值,那么会创建一个新的 `MyCounter` 并设置到 `ThreadLocal` 中。关键在于,一旦 `MyCounter` 被设置到 `ThreadLocal`,那么它将与当前线程...
例如,在分布式服务中,可能会使用分布式事务解决方案如两阶段提交(2PC)、补偿事务(TCC)等,而ThreadLocal则可以辅助管理这些分布式事务的状态。 总结一下,Java事务和ThreadLocal都是Java多线程编程中不可或缺...
Struts2 源码分析主要涉及其在Tomcat启动过程中的初始化步骤以及请求处理流程。首先,我们来看Tomcat启动时Struts2框架如何准备和执行。 在Tomcat启动时,Struts2的Filter文件被加载,具体是`...
### Java中ThreadLocal详解 #### 一、ThreadLocal概述 在Java多线程编程中,`ThreadLocal`是一个非常重要的工具类,它提供了一种在每个线程内部存储线程私有实例的方法。通常情况下,当多个线程共享某个变量时,...