ThreadLocal原理

ThreadLocal应用场景

问题的出现

在学习使用JDBC构建业务逻辑层时,常用来实现的经典案例就是转账操作

在业务层中,转账一般会有如下的步骤:

  1. 验证转出的账号是否存在;
  2. 验证转出的账号的密码是否存在;
  3. 验证转出的账号的余额是否充足;
  4. 验证转入的账号是否存在;
  5. 减少转出的账号的余额;
  6. 增加转入的账号的余额。

在以上1~4个步骤中,任何一个步骤出现错误都可以抛出一个异常而结束转账,不会影响转账结果。

倘若第5步执行成功之后发生了某种异常而结束转账,那么会导致扣钱成功而加钱失败。

不管是验证步骤还是扣钱加钱步骤,这6个步骤都应该被看成一个原子操作。Java中也可以手动开启事务,在第一步之前调用Connection对象的setAutoCommit()方法并在参数括号中填上false,表示将自动提交事务改为手动提交;在步骤6完成后调用Connection对象的commit()方法提交事务,而转账失败时,在捕获语句中调用rollback()回滚事务。

看上去业务层代码逻辑没问题,但是问题出现了:

在步骤5之后模拟一个异常,添加一条代码int num=1/0;。期望中的样子是扣钱后发生异常(by zero),然后在捕获异常的语句中回滚事务;但真正执行时却发现加钱失败,但扣钱依然成功了。回滚失败了吗?

分析原因

在业务层代码中的验证步骤中,调用了数据访问层代码的select方法查询账户是否存在;

在业务层代码中的转账步骤中,调用了数据访问层代码的update方法修改账户余额;

在数据访问层的查询和修改代码中,分别获取了工具类中的Connection对象来进行数据库操作;

在业务逻辑层的转账代码中,也获取了工具类中的Connection对象来开启和提交/回滚事务。

这个时候出现问题了,这4个Connection对象是同一个对象吗

查看工具类代码,创建连接的步骤如下:

  1. 注册驱动(静态代码块);
  2. 创建并获取Connection对象。
  3. 返回Connection对象。

显然,如前文所述,四次获取的Connection连接对象并不是同一个,每次获取都得到了一个新的Connection对象(可以通过在各自的方法中打印Connection对象来验证),而业务层中获取的Connection对象仅仅对业务层做了控制,并不是回滚失败了。

解决问题

第一种可行的方案,也是很容易想到的方案,就是传递Connection对象。既然是因为连接对象不一致导致了问题的出现,那么将连接对象一致化问题不就解决了吗?

在数据访问层的改查方法中,除了传递必要的诸如卡的账号信息等参数,再传递一个Connection对象参数,那么在业务逻辑层调用DAO层方法时就可以把控制事务的Connection对象传递过去,从而保证了使用连接对象的一致性。

虽然这是一种可行的方法但是并不推荐采纳。这种做法会造成接口污染(BadSmell),我们在DAO层定义的数据库操作是为了实现复用,而DAO层的数据库操作接口绑定了Connection对象,这样会造成什么样的结果?

如果以后使用了如Mybatis、Hibernate等框架,他们的连接对象分别是SqlSession和Session,而Connection是JDBC的连接对象,由于DAO层的数据库操作绑定了Connection对象,那么将无法达到复用的目的。并且在面向接口编程中,这样的绑定就会造成接口污染。所以这个方案可行但不可以采纳。


第二种可行的方案,无论是Servers(业务功能)还是数据访问对象,都是在同一个线程(主线程)中作用的,要想实现无论哪一个DAO或Servers都使用同一个连接对象,可以使用ThreadLocal类。

线程拥有一个类似Map的属性——键值对结构<ThreadLocal对象,值>,一个线程共享同一个ThreadLocal,在整个流程中任一环节都可以存值或取值,换言之ThreadLocal实例对象可以在整个线程(单线程)中存储一个共享值。

这样就可以修改一下工具类中创建连接的步骤了:

  1. 创建ThreadLocal对象:

    private static ThreadLocal<Connection> threadlocal=new ThreadLocal<>();

  2. 创建Connection对象,将当前线程中绑定的Connection对象赋给它,每次获取Connection对象时将会得到一个共享值。

    Connection connection=threadlocal.get()

  3. 第2步中的Connection对象自然是空,接下来可以做一个判断,如果connection为空(第一次获取对象)就可以创建连接了。并且,要把赋值后的Connection对象存在threadlocal中,后续才能进行获取。

    if(connection==null){
        connection=DriverManager.getConnection(url,username,password);
        threadlocal.set(connection);
    }
    

总结下来不过多了寥寥几行代码,就可以让整个流程使用同一个Connection对象。

不过需要注意的是,因为只使用了一个Connection对象,所以释放资源的时候只能在最后使用后再释放。如果DAO中有关闭connection的操作,需要拿掉,在业务代码中才能最后进行connection的释放。

ThreadLocal原理

ThreadLocal对象在创建的时候并没有和线程绑定在一起,想要知道它是在什么时候和线程绑定在一起的话,需要先查看Thread类的源码。

Thread线程类有一个属性:

//源码1-1
ThreadLocal.ThreadLocalMap threadLocals = null;

从属性上可以看出ThreadLocalMap是ThreadLocal的一个内部类,它定义了一个该类型的threadLocals属性。

在上一节的解决方案中,通过ThreadLocal实例对象的set方法存储了Connection对象。

//源码1-2
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

在创建ThreadLocal对象时指定了泛型为Connection,所以此刻参数中的value就是connection。源码1-2中第一步拿到了当前线程对象t,然后作为参数传给getMap方法。

//源码1-3
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

该方法返回了当前线程对象t的属性threadLocals,看到源码1-1,该属性初始值为null,所以此处返回值为null。

然后回到源码1-2,此时map的值被赋予了null,接着做了一个判断,为空走else语句块,调用了createMap方法,将当前线程对象t和connection作为value传过去。

//源码1-4
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

该方法为属性threadLocals进行了赋值,调用了ThreadLocalMap的带参方法。第一个参数this指的是在工具类中创建的ThreadLocal对象,第二个参数是传入的connection

//源码1-5
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

该方法首先为一个Entry类型的数组进行了实例化,数组大小为16。关于Entry这个类,它是Map底层数据的存储结构,存储了一个键值对,数组的默认的初始化大小为16,这里不再展开讲解。第三行代码做了一个与运算,也就是用散列求值得到了一个0~15的值又赋给了i。接着new了一个Entry对象传入到对应的大小为16的table数组中,这个Entry对象实例化时传入的值分别为在工具类中创建的ThreadLocal对象connection

回到源码1-4,此时threadLocals属性有值了,也就代表着源码1-1中的属性已经被赋值了。回到源码1-2,set方法执行完createMap后,那么在ThreadLocal类中就有一个Entry类型的数组table该数组存储了工具类中所创建的ThreadLocal对象和connection。这就是执行set方法后,将ThreadLocal对象和connection所绑定的过程。

当ThreadLocal绑定了Connection对象之后,就需要通过get方法进行获取。

//源码1-6
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

该方法可以对比源码1-2查看,get方法同样是先获取了当前线程对象t,然后调用getMap方法;不同的是,看到源码1-3,此时返回的不再是null,因为之前通过set方法调用源码1-4为threadLocals进行了赋值,所以源码1-6中的map不为空,不为空就调用getEntry方法将其返回值赋给Entry对象e。这个参数this是一个key,通过key得到对应的Entry类型的值而这个key就是ThreadLocal对象

得到的e如果不为空就将其值赋给result此处的值就是connection。T就是Connection类型。

总结起来就是:set方法new了一个ThreadLocalMap对象赋给threadLocals,而在new所调用的ThreadLocalMap类中用ThreadLocal对象和connection作为键值对参数创建了一个Entry对象并存进了根据ThreadLocal所计算出来的table下标中。get方法根据ThreadLocal对象得到存储在table中的Entry对象,并得到Entry对象的value,也就是connection。

这个就是ThreadLocal的实现原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值