ThreadLocal与Spring 事务管理

时间:2022-04-25
本文章向大家介绍ThreadLocal与Spring 事务管理,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

编写线程安全代码的关键是管理程序中的共享可变状态,除了通过synchronized加锁机制防止多个线程同时访问同一段数据外,还有一种方法就是通过ThreadLocal消除数据的共享,ThreadLocal会为各自线程创建相应的变量副本(线程局部变量),每个副本都由各自线程管理,这样就避免了对共享资源的访问冲突,也减少了同步时的性能消耗。我们来看一段示例:

class Sequence implements Runnable {
 private final int tid;
 public Sequence(int tid) {
 this.tid = tid;
 }
 public void run() {
 while (!Thread.currentThread().isInterrupted()&&VarHolder.get()<6) {
   VarHolder.increment();
   System.out.println(this);
//提示调度器,让相同优先级的线程获得运行的机会,方便重现竞争条件的情景 
    Thread.yield();
  }
 }
 public String toString() {
 return "tid" + tid + ": " + VarHolder.get();
 }
}
public class VarHolder {
 private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
 protected synchronized Integer initialValue() {
 return 0;
  }
 };
 public static void increment() {
 // set()设置当前线程的局部线程变量的值
 value.set(value.get() + 1);
 }
 public static int get() {
 //get()返回当前线程对应的线程局部变量
 return value.get();
 }
 public static void main(String[] args) throws Exception {
  ExecutorService exec = Executors.newCachedThreadPool();
 for (int i = 0; i < 5; i++){
 exec.execute(new Sequence(i));
  }
  Thread.sleep(2000);
 exec.shutdownNow(); 
 }
}

运行结果:

tid3: 1

tid0: 1

tid1: 1

tid4: 1

tid2: 1

tid3: 2

tid2: 2

tid3: 3

tid4: 2

tid3: 4

tid4: 3

tid1: 2

tid0: 2

tid3: 5

tid2: 3

tid1: 3

tid0: 3

tid4: 4

tid0: 4

tid1: 4

tid2: 4

tid3: 6

tid2: 5

tid1: 5

tid0: 5

tid4: 5

tid2: 6

tid1: 6

tid0: 6

tid4: 6

每个线程都按序输出1-6,执行正常。

ThreadLocal有各种应用场景,比如在Spring中的事务管理模块,ThreadLocal就有精彩的表现。

基于软件工程中的模块化设计原则,我们会将业务操作与数据访问拆分开来,将业务逻辑放在service层,将数据访问模块放在Dao层,service层通过Dao层进行数据访问,而事务管理是放在service层的,这样拆分,提高了模块的重用性,一个service有可能调用若干个dao,而要让多个dao的访问在同一个事务下,则他们必须使用同一个connection,因为性能和并发的要求,connection不会是全局变量,于是我们通过传参的方式把当前connection传到相应的调用的dao方法中。

 public void serviceA(){
  Connection conn = transactionManager.doBegin();
  dao1.doX(conn);
  dao2.doY(conn);
  transactionManager.doEnd(conn);
 }

事务管理代码依然和数据访问层紧密耦合,无法实现重用,假如现在需要的不是JDBC的connection,而是其他资源,比如是hibernate的session,那这一切是不是都要重新调整了?

理想中的调用应该是这样的:

 public void serviceA(){
  Connection  conn= transactionManager.doBegin();
  dao1.doX();
  dao2.doY();
  transactionManager.doEnd(conn);
 }

事务管理层和数据访问服务之间不能直接耦合,在事务开始阶段,将connection与当前线程绑定,数据访问时,从当前线程获取绑定的connection进行操作,等事务提交或回滚后,解除绑定。Spring有两个主要类实现这个功能,AbstractPlatformTransactionManager和TransactionSynchronizationManager,而核心机制就是使用了ThreadLocal。

AbstractPlatformTransactionManager针对不同的数据访问技术,有着不同的实现类,如DataSourceTransactionManager和HibernateTransactionManager,以前者为例:

在doBegin方法中,他将资源(connection)与当前线程绑定起来:

bindResource方法源码如下:

那resources又是神马呢,聪明的你或许已经猜到了,正是我们开始讲的ThreadLocal。

再后续的数据访问中,他就是从当前线程中获取资源(connection)进行操作的。可以看JdbcTemplate的execute()方法源码验证下:

他通过DataSourceUtils获取connection,这个connection又是怎么获得的呢?

他调用TransactionSynchronizationManager的getResource方法获取资源,判断当前线程下是否有绑定的connection,如果没有,则重新从dataSource获取。

Spring事务管理通过使用ThreadLocal,解除了事务管理模块与数据访问层的紧密耦合,提高了模块的可重用性,也保证了多线程环境下的对connection资源的有效管理,实现了线程安全。而要将事务管理代码从整个业务逻辑中抽离出来,提供系统性的服务,还有许多事情要做。Spring 正是通过aop机制解决这个问题的,这个我们下次再讲。