第21章 Spring事务管理之扩展篇

本章内容

  • 理解并活用ThreadLocal

  • 谈Strategy模式在开发过程中的应用

  • Spring与JTA背后的奥秘

21.1 理解并活用ThreadLocal

之前在事务管理器实现原型的讲解中,我们展示了ThreadLocal在Spring的事务管理框架中所起的核心作用,即通过ThreadLocal的使用来避免connection- passing方式最初的尴尬局面。不过,我们依然认为,这只是反映了ThreadLocal使用中的一个侧面。 为了让大家更加深入地理解ThreadLocal,并在之后的开发活动中灵活使用它,特增加了本章内容。

21.1.1 理解ThreadLocal的存在背景

Threadlocal是Java语言提供的用于支持线程局部变量(thread-local variable)的标准实现类。 我们可以在称为Thread-Specifc Storage Pattern的多线程编程模式中一窥ThreadLocal的典型应用场景。

国内的Java技术社区对ThreadLocal的理解可以说是百家争鸣,异常热闹。有的将ThreadLocal和synchronized放在一起讨论,有的说它们之间没有任何关系,还有的说ThreadLocal不是用来解决多线程环境下的对象共享问题的,这些观点通常会令许多开发人员对ThreadLocal更加迷惑。不能说这些观点是不对的,但我认为这些都没有说到点子上!

那么,ThreadLocal到底是为何而生呢?它又是否与之前的观点有什么千丝万缕的联系呢?下面让我带大家一起来探索这些问题的答案。

单单从程序层面来看,我们所编写的代码实际上是在管理系统中各个对象的相关状态。如果不能够对各个对象状态的访问进行合理的管理,对象的状态将被破坏,进而导致系统的不正常运行。 特别是多线程环境下,多个线程可能同时对系统中的单一或者多个对象状态进行访问,如果不能保证在此期间的线程安全,将会把整个系统带向崩溃的边缘。

为了保证整个应用程序的线程安全,我们可以采用多种方式。不过,在此之前,我们不妨先来看看打架与我们管理线程安全问题有何相似之处。现在你就是系统中的一个对象,假设某一天你出门在外遇到三名歹徒(当然,仅仅是假设,不用害怕,呵呵),你毫无退路,只能一搏,你会采用什么样的策略来保证你的人身安全呢?毕竟,我们最终的目的都是保证你这个对象的状态始终处于“正确”的状态,对吧?

策略一,你是个练家子,而且身边并无他人相助,所以,你只能同时对付三名歹徒。为了不受伤害,你是辗转腾挪,左躲右闪,尽量保证每一时刻都只对付一名歹徒,以便最终能够全身而退。如果把三名歹徒对你的攻击顺序比作三个线程的话,你实际上是在用同步(Synchronization)的方式来管理这三个线程对你的访问。

使用同步方式来管理多个线程对对象状态的访问以保证应用程序的线程安全是最常用的方式。 不过,你也看到了,你需要很辛苦地对待,稍一不慎,就有可能受伤。所以,这时我们就在想,我要是孙悟空有多好。

“但见那孙猴子揪一撮猴毛一吹,片刻化作多个分身”,“小的们,给我上”。现在,我们再也不用苦熬了,让一个分身对付一个歹徒(小妖),让他们在各自的线程内各自折腾去吧!不用一对三,当然也就不可能破坏到我的完好状态咯!

这就是策略二,通过避免对象的共享,同样达到线程安全的目的。 你想啊,都在各自的线程内跟各自的分身折腾去了,自然也就不会需要同步对单一共享资源的访问了。ThreadLocal的出现,实际上就是帮助我们以策略二的方式来管理程序的线程安全。只要当前环境允许,能不共享的尽量不共享,反而更容易管理应用程序的线程安全。

综上来说,同步和ThreadLocal在横向上可能没有任何的关系,但从纵向上看,它们实际上都服务于同一个目的,那就是帮助我们实现应用程序的线程安全。 另外,说ThreadLocal不是用来解决多线程环境下对象共享问题的,也就更好解释了。 ThreadLocal的目的是通过避免对象的共享来保证应用程序实现中的线程安全。共享对象是ThreadLocal尽量避免的。 如果要管理的对象非要共享,ThreadLocal自然不会理会这码子事儿了。

21.1.2 理解Threadlocal的实现

我们已经了解了ThreadLocal因何而生,现在该是我们探索ThreadLocal又是如何运作的时候了,它到底是如何来完成它的职责的呢?

虽然是通过ThreadLocal来设置特定于各个线程的数据资源,但ThreadLocal自身不会保存这些特定的数据资源。因为数据资源特定于线程的,自然是由每个线程自己来管理了。每个Thread类都有一个ThreadLocal.ThreadLocalMap类型的名为threadLocals的实例变量,它就是保持那些通过ThreadLocal设置给这个线程的数据资源的地方。当通过ThreadLocal的set(data)方法来设置数据的时候,ThreadLocal会首先获取当前线程的引用,然后通过该引用获取当前线程持有的threadLocals,最后,以当前ThreadLocal作为Key,将要设置的数据设置到当前线程,如下所示:

Threadthread = Thread.currentThread();
ThreadLocalMap threadlocalmap = thread.threadLocals;
...
threadlocalmap.set(this, obj);

而至于余下的get()之类的方法,基本上也是同样的道理,都是首先取得当前线程,然后根据每个方法的语义,对当前线程所持有的threadLocals中的数据进行操作。

实际上,ThreadLocal就好像是一个窗口,通过这个窗口,我们可以将特定于线程的数据资源绑定到当前线程,也可以通过这个窗口获取绑定的数据资源,当然,更可以解除之前绑定到当前线程的数据资源。在整个线程的生命周期内,我们都可以通过ThreadLocal这个窗口与当前线程打交道。

为了更好地理解Thread与ThreadLocal之间的关系,我们不妨设想一下城市的公交系统(见图21-1)。城市中的各条公交线路就好像我们系统中的那一一个个线程,在各条公交线路上,会有相应的公交车辆,这些公交车辆就好像Thread的threadLocals,用来运送特定于该条线路的乘客(数据资源)。为了乘客可以乘车或者下车,各条公交线路沿线都会设置多个乘车点(Bus Stop),而这些乘车点实际上就是ThreadLocal。虽然同一个乘车点可能会有多条公交线路共用,但同一时间,乘客只会搭乘他要乘坐并且当前经过的公交车。

这与ThreadLocal和Thread的关系是类似的,虽然同一个ThreadLocal可以为多个线程指定数据资源,但只会将数据资源指定到当前的线程。至此,你应该再也不会感觉ThreadLocal神秘了吧?

image-20220624154615637

21.1.3 ThreadLocal的应用场景

ThreadLocal的概念和提供的功能很简单,但如果能够充分发挥ThreadLocal的能力,将会为我们的开发工作带来意想不到的效果。基本上,我们可以从两个方面来看待并灵活应用Threadlocal(见图21-2)。

image-20220624155114472

  • 横向上看,我们是更注重于ThreadLocal 横跨多个线程 的能力,这当然是ThreadLocal最初的目的所在。为了以** 更加简单地方式来管理应用程序的线程安全,**ThreadLocal干脆将没有必要共享的对象不共享,直接为每个线程分配一份各自特定的数据资源。

  • 纵向上看,我们则着眼于ThreadLocal在单一线程内可以发挥的能力, 通过ThreadLocal设置的特定于各个线程的数据资源,可以随着所在线程的执行流程“随波逐流”。

当然,两个方面不是严格独立的,更多时候它们则是相互依存、紧密结合的。

在充分发挥ThreadLocal两方面能力的基础上,我们可以发掘出以下几种ThreadLocal的应用场景。

  • 管理应用程序实现中的线程安全。 对于某些有状态的或者非线程安全的对象,我们可以在多线程程序中为每个线程都分配相应的副本,而不是让多个线程共享该类型的某个对象,从而避免了需要协调多个线程对这些对象进行访问的“危险”工作。

在使用JDBC进行数据访问的过程中,Connection对象就属于那种有状态并且非线程安全的类。所以,为了保证多个线程使用Connection进行数据访问过程中的安全,我们通过ThreadLocal为每个线程分配了一个它们各自持有的Connection,从而避免了对单一Connection资源的争用。毕竟,在JDBC中是一个Connection对应一个事务。如果所有的线程都共用一个Connection的话,那么整个事务管理就有点儿失控的感觉了。在之前部分,当以简化的形式向读者展示Spring的事务框架是如何对Connection进行管理的时候,我们只是强调了使用ThreadLocal来避免“connection passing”的尴尬,而实际上,通过ThreadLocal来保证Connection在多线程环境下的正确使用,应该也是Spring的事务框架使用ThreadLocal进行Connection管理的原因之一。

  • 实现当前程序执行流程内的数据传递。 这种场景下,我们更多关注的是在单一的线程环境中使用ThreadLocal。在剥离线程安全等因素的考虑之后,“connection passing”实际上就可以看作这个场景下的一例。

除此之外, 我们也可以通过ThreadLocal来跟踪保存在线程内的日志序列 ,在程序执行的任何必要执行点将系统跟踪信息添加到ThreadLocal,然后在合适的时点取出分析。我们还可以通过ThreadLocal来保存某种全局变量,在线程内执行流程的某个时点设置该全局变量,然后在合适的位置获取其值以做某些判断工作等。只要你愿意,任何合适的数据都可以通过“ThreadLocal在单一线程内传递数据”这一功能进行传递。

采用ThreadLocal进行当前执行流程内的数据传递,可以避免耦合性很强的方法参数形式的数据传递方式。但这有些像是让数据随着“暗流”漂泊的意思,一旦处理不当就会出现“触礁”之类的事故,比如资源没有适当的清理导致系统行为差异。所以,通常应该通过一组框架类来规范并屏蔽对ThreadLocal的直接操作,尽量避免应用代码的直接接触。

  • 某些情况下的性能优化。 有些情况下,系统中一些没有必要共享的对象被设置成了共享,为了保证应用程序的线程安全以及对象状态的正确,我们往往就得通过同步等方式对多线程的访问进行管理和控制。这时,各个线程在走到这个共享对象的时候就得排队,一个一个地对该共享对象进行访问。显然,这将严重影响系统的处理性能,比如去银行取款的时候,就一个窗口在营业,而你又急着取钱,可以想象一下你现在是一种什么样的心情。所以,能够避免共享的时候,就尽量不要共享,多开几个营业窗口,要比单一营业窗口的处理速度快得多。某些情况下,通过ThreadLocal这种“以空间换时间”的方式来管理对象访问,可以收到更好的响应效果。

  • per-threadSingleton。 当某项资源的初始化代价有些大,并且在整个执行流程中还会多次访问它的时候,为了避免在访问时每次都需要去初始化该项资源,我们可以在第一次将该资源初始化完成之后,直接通过ThreadLocal将其绑定到当前线程,之后,所有对该资源的访问都从当前线程获取即可。

这实际上与“实现当前程序执行流程内的数据传递”的应用场景很相似。不过,该场景更侧重于资源管理,所以,单独罗列在此也不为过了。

我想,灵活运用ThreadLocal可以在更多场景中发挥作用,故希望大家也能够在平时的开发过程中发掘出更多ThreadLocal的应用场景。

21.1.4 使用ThreadLocal管理多数据源切换的条件

第16章,介绍了如何实现一一个简单的AbstractRountingDataSource原型来管理多个数据源之间的切换功能。当时的原型实现可能让你觉得过于简单而不甚真实。 这里我们引入ThreadLocal来协助管理多数据源切换的条件,以期抛砖引玉,使得你在日常的开发工作中灵活运用ThreadLocal带给我们的便利。

在多数据源的切换过程中,切换的条件可能随着应用程序的需求而各异,而且,通常不会像我们的AbstractRoutingDataSource原型实现那样,只需要内部条件就可以实现数据源切换的判断。更多时候,需要外部条件的介入,这就会有一个问题, 如何为AbstractRoutingDataSource的实现子类传入这些外部条件相关的数据?

ThreadLocal这个时候就可以派上用场。我们的思路是,通过ThreadLocal保存每个数据源所对应的标志(该标志我们以枚举类的形式给出),AbstractRoutingDataSource在通过determineCurrentLookupKey()获取对应数据源的键值的时候,直接从ThreadLocal获取当前线程所持有的数据源对应标志然后返回。而至于说什么情况下使用哪个具体的数据源的问题,则是由应用程序的需求来决定,只要在必要的地方,将所要使用的具体数据源的对应标志通过ThreadLocal绑定到当前线程即可。

使用ThreadLocal管理多数据源切换条件的AbstractRoutingDataSource实现流程如下。

(1)假设我们有MAIN、INFO和DBLINK三个数据源可用,第一步要做的事情就是先给出一个枚举类,其中定义了对应这三个数据源的标志,如下所示:

public enum DataSources {
	MAIN,INFO,DBLINK;
}

(2)我们定义所使用的ThreadLocal(见下方代码清单),没有“车站”,我们可没法上车啊!

持有ThreadLocal的DataSourceTypeManager类定义:

public class DataSourceTypeManager {
	private static final ThreadLocal<DataSources> dsTypes = new ThreadLocal<DataSources>() {
		@Override
		protectedDataSourcesinitialValue() {
			returnDataSources.MAIN;
    }
  };

	public static DataSources get() {
		return dsTypes.get();
  }

	public static void set(DataSources dataSourceType) {
		dsTypes.set(dataSourceType);
  }

	public static void reset() {
		dsTypes.set(DataSources.MAIN);
  }
}

(3)有了标志枚举类和相应的ThreadLocal定义之后,就可以实现我们的AbstractRoutingDataSource了,如下代码所示:

public class ThreadLocalVariableRountingDataSource extends AbstractRoutingDataSource {
  @Override
	protected Object determineCurrentLookupKey() {
		return DataSourceTypeManager.get();
  }
}

(4)现在我们需要将ThreadLocalVariableRountingDataSource以及相关的依赖注册到IoC容器中,如下方代码清单所示。

<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value="."/>
</bean>

<bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value="."/>
</bean>

<bean id="db1inkDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="url" value=".."/>
	<property name="driverClassName" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value="."/>
</bean>

<bean id="dataSource" class="..ThreadLocalVariableRountingDataSource">
	<property name="defaultTargetDataSource"ref=""/>
	<property name="targetDataSources">
		<map key-type="..DataSources">
			<entry key="MATN"value-ref="mainDataSource"/>
			<entry key="INFO"value-ref="infoDataSource"/>
			<entry key="DBLINK"value-ref="dblinkDataSource"/>
		</map>
	</property>
</bean>

注意,我们在配置ThreadLocalVariableRountingDataSource所使用的多个目标数据源的时候,使用了<map>key- type属性指明了键值的类型,否则,就得通过其他的方式来确定枚举类的各值作为key与目标数据源之间的对应关系。

(5)万事俱备之后,我们就可以使用如下代码使得数据源的切换生效:

DataSourceTypeManager.set(DataSources.INFO);
// 或者
DataSourceTypeManager.set(DataSources.DBLINK);
...

此后通过ThreadLocalVariableRountingDataSource所进行的数据访问,则会使用我们所设定的具体数据源。

至于说以上代码要放在什么地方,可以因应用程序而异,比如我们可以在程序或者某个线程启动之后进行设置,也可以在某个AOP的拦截器中,根据条件判断来进行设定以实现数据源的切换,等等。

至此,打完收功。怎么样?大家现在是否对ThreadLocal的应用跃跃欲试了呢?