《Struts2技术内幕》 新书部分篇章连载(七)—— ThreadLocal模式
第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)。运行之后,我们可以看一下这个例子的输出:
从上面图中我们可以看到,由于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所示:
这是一个简单的线程类,循环输出当前线程的名称和getNextCounter的结果,由于getNextCounter中的逻辑所操作的是ThreadLocal中的变量,所以无论同时有多少个线程在运行,返回的值将仅与当前线程的变量值有关,也就是说,在同一个线程中,变量值会被连续累加。这一点可以通过如下的测试代码证实:Thread[Thread-2],counter=11Thread[Thread-2],counter=12Thread[Thread-2],counter=13Thread[Thread-0],counter=11Thread[Thread-0],counter=12Thread[Thread-0],counter=13Thread[Thread-1],counter=11Thread[Thread-1],counter=12Thread[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事件处理体系设计的核心。 1 楼 Rhain 2012-01-07 请问ThreadLocal是否可以用来处理分页数据的共享如:“当前页,页面显示的个数”,每次的请求都会附带这两个参数,在控制层就通过ThreadLoacl来获取这两个参数的值? 2 楼 downpour 2012-01-07 Rhain 写道请问ThreadLocal是否可以用来处理分页数据的共享如:“当前页,页面显示的个数”,每次的请求都会附带这两个参数,在控制层就通过ThreadLoacl来获取这两个参数的值?
其实我在这里说得非常清楚,数据传递和数据共享是完全不同的概念。ThreadLocal只能解决不同编程层次的数据共享问题而不能代替数据传递。
你这个问题大约在05年的时候在javaeye上有很激烈的讨论,正反各执一词。我的观点是,Page信息应视作参数进行传递,而非在所有层次进行共享。这里有2个方面的原因:
1. 将page信息作为参数进行传递能够使得业务逻辑层的调用接口的契约更加明确。
从外界来看,如果page信息不在你的接口定义之中,调用者将无法知晓你的这个接口到底在做一个什么样的事情。
2. 将page信息置于ThreadLocal中,并在其他的编程层次读取,使得每一个编程层次都严格绑定在了当前线程之中,这样一来你每一个层次的程序都无法单独进行单元测试。
这一点就非常致命了,你可以在实际编程中仔细想一想。 3 楼 bill2004158 2012-01-08 使用ThreadLocal模式的前提是处理各层函数调用的必须是同一个线程?
也就是现在流行的异步处理就不适用了? 4 楼 downpour 2012-01-09 bill2004158 写道使用ThreadLocal模式的前提是处理各层函数调用的必须是同一个线程?
也就是现在流行的异步处理就不适用了?
我们这里讨论的Web开发框架与异步开发框架的设计理念是不同的。ThreadLocal只是一个常见的Java技术,不用把它放到框架的层面去考虑。 5 楼 Rhain 2012-01-09 多谢指点!顺便问下为什么你的struts2技术内幕还是在预定中,咋还不能购买呀?
6 楼 aris2012 2012-02-15 从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
7 楼 downpour 2012-02-15 aris2012 写道从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)。
第一条,非静态ThreadLocal变量也可以吧?
恩,只要满足共享的数据环境即可。 8 楼 873339698 2012-03-01 对threadlocal有了更深的理解了.
博文中一点点小瑕疵,作者勿怪
菱形部分--->平行四边形