结合ThreadLocal来看spring事务源码,感受下清泉般的洗涤!

  在我的博客spring事务源码解析中,提到了一个很关键的点:将connection绑定到当前线程来保证这个线程中的数据库操作用的是同一个connection。但是没有细致的讲到如何绑定,以及为什么这么绑定;另外也没有讲到连接池的相关问题:如何从连接池获取,如何归还连接到连接池等等。那么下面就请听我慢慢道来。

ThreadLocal

  讲spring事务之前,我们先来看看ThreadLocal,它在spring事务中是占据着比较重要的地位;不管你对ThreadLocal熟悉与否,且都静下心来听我唐僧般的念叨。

  先强调一点:ThreadLocal不是用来解决共享变量问题的,它与多线程的并发问题没有任何关系。

基本介绍

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,如下例:

public class ThreadLocalTest
{
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
    
    public void set()
    {
        longLocal.set(1L);
        stringLocal.set(Thread.currentThread().getName());
    }
    
    public long getLong()
    {
        return longLocal.get();
    }
    
    public String getString()
    {
        return stringLocal.get();
    }
    
    public static void main(String[] args) throws InterruptedException
    {
        final ThreadLocalTest test = new ThreadLocalTest();
        
        test.set();     // 初始化ThreadLocal
        for (int i=0; i<10; i++)
        {
            System.out.println(test.getString() + " : " + test.getLong() + i);
        }
        
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                for (int i=0; i<10; i++)
                {
                    System.out.println(test.getString() + " : " + test.getLong() + i);
                }
            };
        };
        thread1.start();
        
        Thread thread2 = new Thread(){
            public void run() {
                test.set();
                for (int i=0; i<10; i++)
                {
                    System.out.println(test.getString() + " : " + test.getLong() + i);
                }
            };
        };
        thread2.start();
    }
}

    执行结果如下

    可以看到,各个线程的longLocal值与stringLocal值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

源码解读

    这里我就不进行ThreadLocal的源码解析,建议大家去看我参考的博客,个人认为看那两篇博客就能对ThreadLocal有个很深地认知了。

    做个重复的强调(引用[Java并发包学习七]解密ThreadLocal中的一段话):

Thread与ThreadLocal对象之间的引用关系图
 

Spring事务中的ThreadLocal

  最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等,那么接下来我们就看看spring事务中ThreadLocal的应用

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-jdbc.xml");
DaoImpl daoImpl = (DaoImpl) ac.getBean("daoImpl");
System.out.println(daoImpl.insertUser("yes", 25));

  只要某个类的方法、类或者接口上有事务配置,spring就会对该类的实例生成代理。所以daoImpl是DaoImpl实例的代理实例的引用,而不是DaoImpl的实例(目标实例)的引用;当我们调用目标实例的方法时,实际调用的是代理实例对应的方法,若目标方法没有被@Transactional(或aop注解,当然这里不涉及aop)修饰,那么代理方法直接反射调用目标方法,若目标方法被@Transactional修饰,那么代理方法会先执行增强(例如判断当前线程是否存在connection,不存在则新建并绑定到当前线程等等),然后通过反射执行目标方法,最后回到代理方法执行增强(例如,事务回滚或事务提交、connection归还到连接池等等处理)。这里的绑定connection到当前线程就用到了ThreadLocal,我们来看看源码

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    Connection con = null;

    try {
        
        if (txObject.getConnectionHolder() == null ||
                txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            // 从连接池获取一个connection
            Connection newCon = this.dataSource.getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
            }
            // 包装newCon,并赋值到txObject,并标记是新的ConnectionHolder
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }

        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();

        Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
        txObject.setPreviousIsolationLevel(previousIsolationLevel);

        // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
        // so we don't want to do it unnecessarily (for example if we've explicitly
        // configured the connection pool to set it already).
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
            }
            con.setAutoCommit(false);
        }
        txObject.getConnectionHolder().setTransactionActive(true);

        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }

        // 若是新的ConnectionHolder,则将它绑定到当前线程中
        // Bind the session holder to the thread.
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
        }
    }

    catch (Throwable ex) {
        if (txObject.isNewConnectionHolder()) {
            DataSourceUtils.releaseConnection(con, this.dataSource);
            txObject.setConnectionHolder(null, false);
        }
        throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
    }
}
/**
 * Bind the given resource for the given key to the current thread.
 * @param key the key to bind the value to (usually the resource factory)
 * @param value the value to bind (usually the active resource object)
 * @throws IllegalStateException if there is already a value bound to the thread
 * @see ResourceTransactionManager#getResourceFactory()
 */
public static void bindResource(Object key, Object value) throws IllegalStateException {        //key:通常指资源工厂,也就是connection工厂,value:通常指活动的资源,也就是活动的ConnectionHolder
    
    // 必要时unwrap给定的连接池; 否则按原样返回给定的连接池。
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    Assert.notNull(value, "Value must not be null");
    Map<Object, Object> map = resources.get();
    // set ThreadLocal Map if none found   如果ThreadLocal Map不存在则新建,并将其设置到resources中
    // private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
  // 这就到了ThreadLocal流程了
    if (map == null) {
        map = new HashMap<Object, Object>();
        resources.set(map);
    }
    Object oldValue = map.put(actualKey, value);
    // Transparently suppress a ResourceHolder that was marked as void...
    if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
        oldValue = null;
    }
    if (oldValue != null) {
        throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
                actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
                Thread.currentThread().getName() + "]");
    }
}

总结

  1、ThreadLocal能解决的问题,那肯定不是共享变量(多线程并发)问题,只是看起来有些像并发;像火车票、电影票这样的真正的共享变量的问题用ThreadLocal是解决不了的,同一时间,同一趟车的同一个座位,你敢用ThreadLocal来解决吗?

  2、每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object

  3、druid连接池用的是数组来存放的connectionHolder,不是我认为的list,connectionHolder从线程中解绑后,归还到数组连接池中;connectionHolder是connection的封装

疑问

  private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<Map<Object, Object>>("Transactional resources");

  1、 为什么是ThreadLocal<Map<Object, Object>>,而不是ThreadLocal<ConnectionHolder>

  2、 ThreadLocal<Map<Object, Object>> 中的Map的key是为什么是DataSource

  望知道的朋友赐教下,评论留言或者私信都可以,谢谢!

参考 

[Java并发包学习七]解密ThreadLocal

Java并发编程:深入剖析ThreadLocal

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序猿DD

Spring框架中的设计模式(四)​

本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨论单...

40460
来自专栏服务端技术杂谈

InheritableThreadLocal源码阅读

在进行多线程编程时,我们经常需要线程池子线程和父线程进行ThreadLocal信息传递,实现一些业务处理。 先看一个例子 public class App { ...

28840
来自专栏Java与Android技术栈

RxCache 整合 Android 的持久层框架 greenDAO、Room一. 背景二. 持久层三. 使用四. 总结

RxCache 是一个支持 Java 和 Android 的 Local Cache 。

14820
来自专栏Flutter入门

Weex是如何在Android客户端上跑起来的

Weex可以通过自己设计的DSL,书写.we文件或者.vue文件来开发界面,整个页面书写分成了3段,template、style、script,借鉴了成熟的MV...

49050
来自专栏岑玉海

Carbondata源码系列(一)文件生成过程

在滴滴的两年一直在加班,人也变懒了,就很少再写博客了,最近在进行Carbondata和hive集成方面的工作,于是乎需要对Carbondata进行深入的研究。 ...

65560
来自专栏大内老A

为ASP.NET MVC创建一个基于Unity的ControllerFactory

谈到IoC和ASP.NET的集成,很多人会先后想到Ninject,不过我们个人还是倾向于Unity。这篇文章简单地介绍如果创建基于Unity的Controlle...

19870
来自专栏跟着阿笨一起玩NET

以读取博客园随笔备份为例 将xml 序列化成json,再序列化成对象

资源下载:http://files.cnblogs.com/codealone/ConsoleApplication2.zip

10910
来自专栏Phoenix的Android之旅

深入分析Java的ThreadLocal

上回书说,Android可以用Looper+Handler来实现线程通信的关键是在于Looper 回顾:深入了解Android的Looper Looper 在当...

7820
来自专栏chenssy

【死磕Sharding-jdbc】---强制路由

位于 sharding-jdbc-core模块下的包 com.dangdang.ddframe.rdb.sharding.hint中,核心类HintManage...

25410
来自专栏企鹅号快讯

聊一聊 Spring 中的线程安全性

Spring与线程安全 Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发...

20860

扫码关注云+社区

领取腾讯云代金券