Java 并发 (2) -- ThreadLocal 类

1. 简介

  1. ThreadLocal 与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而 ThreadLocal 是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响到其他线程所对应的副本

  2. 需要特别注意的是,ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在的,而是为了方便每个线程处理自己的状态而引入的一种机制

  3. ThreadLocalMap 是 ThreadLocal 中的一个静态内部类,同时也是实现 ThreadLocal 的关键。ThreadLocalMap 的内部利用 Entry 类来实现 key-value 的存储,key 为当前 ThreadLocal 对象,value 则是对应线程的变量副本,也就是说,ThreadLocal 对象本身是不存储值的,它只是提供了一个在当前线程中找到副本值的 key

  4. ThreadLocal 四个常用的方法:

    1. get():返回当前线程中对应的变量副本的值,该方法首先通过当前线程和 getMap() 获取对应的 ThreadLocalMap 类型的成员变量 map ,然后通过 map 获取当前 ThreadLocal 的 Entry,最后通过所获取的 Entry 获取目标值 result 并返回
    2. remove():移除此线程局部变量中当前线程的值,该方法同上理先获取 map 变量,然后判断 map 是否为空,如果不为空,则移除,否则不做任何操作
    3. set(T value):将此线程局部变量对应的当前线程副本中的值设置为指定值,该方法同上理先获取 map 变量,然后判断 map 是否为空,如果不为空,则调用 ThreadLocalMap 的 set() 方法,key 就是当前 ThreadLocal 对象;如果为空,则调用 createMap() 方法新建一个 ThreadLocalMap 对象,该方法有两个参数,第一个参数为 ThreadLocal 对象,第二个参数为指定的 value
    4. initialValue():返回此线程局部变量的当前线程的初始值,该方法的访问权限是 protected 且返回值为 null,所以,很明显是要子类去实现它的,我们在使用 ThreadLocal 的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用 get() 或 set() 方法时才会被自动执行,并且只执行 1 次
  5. ThreadLocal 还存在一个内存泄漏的问题:因为每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 的 map,该 map 的 key 为 ThreadLocal 实例,它是一个弱引用,我们知道弱引用有利于 GC 回收。当 key == null 时,GC 就会回收这部分空间,但是 value 却不一定能够被回收,因为他还与当前线程存在一个强引用关系,这样会导致一种现象:key 为 null,value 有值,key 为空的话 value 是无效数据,久而久之,value 累加就会导致内存泄漏;只有当当前线程销毁时,value 才能得到释放,因此,只要当线程对象被 GC 回收时,才不会出现内存泄露,但在 threadLocal 设为 null 到线程结束这段时间内,value 是不会被回收的;更槽糕的是对于线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的而是被反复使用的,此时就可能出现内存泄露

    解决方案:(主要体现在两个方面)

    1. 每次使用完 ThreadLocal 都显示调用它的 remove() 方法清除数据。因为它的 remove() 方法会主动将当前的 key 和 value 进行清除

    2. ThreadLocal 的设计者也意识到了这一点(内存泄漏), 他们在一些方法中做了一些对 key == null 时 value 的擦除操作;当我们调用 ThreadLocal 的 set()和 get() 方法时,会进一步去调用 ThreadLocalMap 的 setEntry() 和 getEntry() 这些方法会对 key 进行了校验同时对 key 为 null 的value 置为 null

      当然这样做, 也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建 ThreadLocal 但不调用 set、get、remove 方法。所以最能解决问题的办法就是用完 ThreadLocal 后手动调用 remove()

    手动释放 ThreadLocal 遗留存储 ? 你怎么去设计/实现 ?

    这里主要是强化一下手动 remove() 的思想和必要性,设计思想与连接池类似:

    包装其父类 remove 方法为静态方法,如果是 Spring 项目, 可以借助于 bean 的生命周期, 在拦截器的afterCompletion 阶段进行调用

    弱引用导致内存泄漏,那为什么 key 不设置为强引用 ?

    如果 key 设置为强引用, 当 threadLocal 实例释放后, threadLocal == null, 但是 threadLocal 会有强引用指向 threadLocalMap,threadLocalMap.Entry 又强引用 threadLocal, 这样会导致 threadLocal 不能正常被 GC 回收

    弱引用虽然会引起内存泄漏, 但是也有 set、get、remove 方法操作对 key 为 null 的 value 进行擦除的补救措施, 方案上略胜一筹

    线程执行结束后会不会自动清空 Entry 的 value ?

    事实上,当 currentThread 执行结束后, threadLocalMap 变得不可达从而被回收,Entry 等也就都被回收了,但这个环境就要求不对 Thread 进行复用,但是我们项目中经常会复用线程来提高性能, 所以 currentThread 一般不会处于终止状态

    Thread 和 ThreadLocal 有什么联系呢 ?

    Thread 和 ThreadLocal 是绑定的, ThreadLocal 依赖于 Thread 去执行, Thread 将需要隔离的数据存放到 ThreadLocal,准确的讲是 ThreadLocalMap中, 进而来实现多线程处理

    Spring 如何处理 Bean 多线程下的并发问题 ?

    ThreadLocal 可以解决相同变量的访问冲突问题, 所以这个对于 Spring 的默认单例 bean 的多线程访问是一个完美的解决方案。Spring 中也确实是用了 ThreadLocal 来处理多线程下相同变量并发的线程安全问题

    Spring 如何保证数据库事务在同一个连接下执行的?

    要想实现 JDBC 事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。至于 Spring 如何保证数据库事务在同一个连接下执行

    首先,DataSourceTransactionManager 是 Spring 的数据源事务管理器, 它会在你调用 getConnection() 的时候会从数据库连接池中获取一个 connection, 然后将其与 ThreadLocal 绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成

关于 ThreadLocal 与 InheritableThreadLocal 比较不错的文章

2. 精讲

1. ThreadLocal 是什么

ThreadLocal 是啥?以前面试别人时就喜欢问这个,有些伙伴喜欢把它和线程同步机制混为一谈,事实上 ThreadLocal 与线程同步无关。ThreadLocal 虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么 ThreadLocal 到底是什么呢?

API是这样介绍它的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
.
翻译过来就是:
.
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 **set **方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联

所以 ThreadLocal 与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而 ThreadLocal 是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说 ThreadLocal 为多线程环境下的变量问题提供了另外一种解决思路。

ThreadLocal 四个常用的方法:

  1. get():返回此线程局部变量的当前线程副本中的值;

  2. initialValue():返回此线程局部变量的当前线程的“初始值”;

  3. remove():移除此线程局部变量中当前线程的值;

  4. set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

除了这四个方法,ThreadLocal 内部还有一个静态内部类 ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get() 、set() 、remove() 都是基于该内部类操作。ThreadLocalMap 提供了一种以键值对的方式来存储每一个线程的变量副本的方法,key 为当前 ThreadLocal 对象,value 则是对应线程的变量副本

对于 ThreadLocal 需要注意的有两点:

  1. ThreadLocal 实例本身是不存储值,它只是提供了一个在当前线程中找到副本值的 key

  2. ThreadLocal 包含在 Thread 中,而不是 Thread 包含在 ThreadLocal 中,有些小伙伴会弄错他们的关系

下图是 Thread 、ThreadLocal 、ThreadLocalMap 的关系:
在这里插入图片描述
示例:

public class SeqCount {

    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
        // 实现initialValue
        public Integer initialValue(){
            return 0;
        }
    };

    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);
        return seqCount.get();
    }

    public static void main(String[] args) {
        SeqCount seqCount = new SeqCount();
        
        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

public class SeqThread extends Thread {

    private SeqCount seqCount;

    SeqThread(SeqCount seqCount) {
        this.seqCount = seqCount;
    }

    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
        }
    }
}

//运行结果
Thread-0 seqCount :1
Thread-2 seqCount :1
Thread-3 seqCount :1
Thread-1 seqCount :1
Thread-3 seqCount :2
Thread-2 seqCount :2
Thread-0 seqCount :2
Thread-2 seqCount :3
Thread-3 seqCount :3
Thread-1 seqCount :2
Thread-0 seqCount :3
Thread-1 seqCount :3

从运行结果可以看出,ThreadLocal 确实是可以达到线程隔离机制,确保变量的安全性。这里我们想一个问题,在上面的代码中 ThreadLocal 的 initialValue() 方法返回的是 0,假如该方法返回的是一个对象呢,会产生什么后果呢?例如:

A a = new A();
private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
    // 实现initialValue()
    public A initialValue() {
        return a; //每次ThreadLocal的初始化都是返回同一个对象引用 a
    }
};
 
class A{
    // ....
}

ThreadLocal 让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象呢?这个时候 ThreadLocal 就失效了。仔细观察下图中的代码,你会发现,threadLocal 在初始化时返回的都是同一个对象 a

对 ThreadLocal 实现原理的一点思考

2. ThreadLocal 源码分析

ThreadLocal 虽然解决了多线程变量的复杂问题,但是它的源码实现却是比较简单的。ThreadLocalMap 是实现 ThreadLocal 的关键,我们先从它入手

1. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 中的一个静态内部类。ThreadLocalMap 其内部利用 Entry 来实现 key-value 的存储,如下:

public class ThreadLocal<T> {    
    // ... 其他方法        
    static class ThreadLocalMap { 
        //Entry 是一个弱引用
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
            }
        }    
        // ...其他方法
    } 
    // ...其他方法
}

从上面代码中可以看出 Entry 的 key 就是 ThreadLocal,而 value 就是值。同时,Entry 也继承 WeakReference,所以说 Entry 所对应 key(ThreadLocal 实例)的引用为一个弱引用(关于弱引用这里就不多说了,感兴趣的可以关注这篇博客:Java 理论与实践: 用弱引用堵住内存泄漏

ThreadLocalMap 的源码稍微多了点,我们就看两个最核心的方法 getEntry() 、set(ThreadLocal<?> key, Object value) 方法:

1. getEntry()
private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

由于采用了开放定址法,所以当前 key 的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的 key 就是我们所要找的元素,则返回,否则调用 getEntryAfterMiss(),如下:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
 
    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

这里有一个重要的地方,当 key == null 时,调用了 expungeStaleEntry() 方法,该方法用于处理 key == null,有利于 GC 回收,能够有效地避免内存泄漏

2. set()
private void set(ThreadLocal<?> key, Object value) {

    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
    int i = key.threadLocalHashCode & (len - 1);

    // 采用“线性探测法”,寻找合适位置
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        ThreadLocal<?> k = e.get();

        // key 存在,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }

       // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
        if (k == null) {
            // 用新元素替换陈旧的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    int sz = ++size;

    // cleanSomeSlots 清除陈旧的Entry(key == null)
    // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

这个 set() 操作和我们在集合了解的put() 方式有点儿不一样,虽然他们都是 key-value 结构,不同在于他们解决散列冲突的方式不同。集合 Map 的 put() 采用的是拉链法,而 ThreadLocalMap 的 set() 则是采用开放定址法(具体请参考散列冲突处理系列博客)。掌握了开放地址法该方法就一目了然了。

set() 操作除了存储元素外,还有一个很重要的作用,就是 replaceStaleEntry()cleanSomeSlots(),这两个方法可以清除掉 key == null 的实例,防止内存泄漏。在 set() 方法中还有一个变量很重要:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();

从名字上面我们可以看出 threadLocalHashCode 应该是 ThreadLocal 的散列值,定义为 final,表示 ThreadLocal 一旦创建其散列值就已经确定了,生成过程则是调用 nextHashCode()

private static AtomicInteger nextHashCode = new AtomicInteger(); 
private static final int HASH_INCREMENT = 0x61c88647; 
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

nextHashCode 表示分配下一个 ThreadLocal 实例的 threadLocalHashCode 的值,HASH_INCREMENT 则表示分配两个 ThradLocal 实例的 threadLocalHashCode 的增量,从 nextHashCode 就可以看出他们的定义。

上面的两个方法都是静态内部类 ThreadLocalMap 中的方法,下面让我们看看 ThreadLocal 中的其他方法

2. ThreadLocal

1. get()

返回此线程局部变量的当前线程副本中的值

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();

    // 获取当前线程的成员变量 threadLocal
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从当前线程的ThreadLocalMap获取相对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取目标值        
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先通过当前线程获取所对应的成员变量 ThreadLocalMap,然后通过 ThreadLocalMap 获取当前 ThreadLocal 的 Entry,最后通过所获取的 Entry 获取目标值 result

getMap() 方法可以获取当前线程所对应的 ThreadLocalMap,如下:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
2. set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值

public void set(T value) {
    Thread t = Thread.currentThread();   // 获取当前线程
    ThreadLocalMap map = getMap(t);  // 获取当前线程所对应的ThreadLocalMap
    if (map != null)
        map.set(this, value);  // ThreadLocalMap不为空时,调用ThreadLocalMap的set()方法
    else
        // ThreadLocalMap为空时,调用createMap()方法新建一个ThreadLocalMap
        createMap(t, value);   
}

首先获取当前线程所对应的 ThreadLocalMap,如果不为空,则调用 ThreadLocalMap 的 set() 方法,key 就是当前 ThreadLocal;如果不存在,则调用 createMap() 方法新建一个ThreadLocalMap,key 为 ThreadLocal 对象,值为指定的 value。如下:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3. initialValue()

返回此线程局部变量的当前线程的 “初始值”

protected T initialValue() {
    return null;
}

该方法定义为 protected 级别且返回为 null,很明显是要子类实现它的,所以我们在使用 ThreadLocal 的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用 get() 或 set() 方法时才会被自动执行,并且仅执行 1 次

4. remove()

将当前线程局部变量的值移除

public void remove() {
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread()); 
    if (m != null)
        m.remove(this);  // 如果当前线程的ThreadLocalMap不为空,则移除
}

3. ThreadLocal 为什么会有内存泄漏

先看这样一个小案例:

在 Java 里面,存在强引用 、弱引用 、软引用 、虚引用。这里主要谈一下强引用和弱引用。

  • 强引用类似于:
    ​ A a = new A();
    ​ B b = new B();

  • 现在考虑这种情况:
    ​ C c = new C(b);
    ​ b = null;
    .

考虑下 GC 的情况。要知道 b 被置为 null,那么是否意味着一段时间后 GC 工作可以回收 b 所分配的内存空间呢?
.
答案是否定的,因为即便 b 被置为 null,但是 c 仍然持有对 b 的引用,而且还是强引用,所以 GC 不会回收 b 原先所分配的空间!既不能回收利用,又不能使用,这就造成了内存泄露
.
那么如何处理呢?
.
可以使 c = null;也可以使用弱引用!WeakReference w = new WeakReference(b);

下面就来看看 ThreadLocal 的内存泄露是怎么一回事:

前面提到每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 的 map,该 map 的 key 为 ThreadLocal 实例,它(key)是一个弱引用,我们知道弱引用有利于 GC 回收。当 ThreadLocal 的 key == null 时,GC 就会回收这部分空间,但是 value 却不一定能够被回收,因为他还与 Current Thread 存在一个强引用关系,这样会导致一种现象:key 为 null,value 有值,key 为空的话 value 是无效数据,久而久之,value 累加就会导致内存泄漏
在这里插入图片描述
只有当 Current Thread 销毁时,value 才能得到释放,因此,只要当这个线程对象被 GC 回收时,才不会出现内存泄露,但在 threadLocal 设为 null 到线程结束这段时间内,value 是不会被回收的,就发生了我们认为的内存泄露。最要命的是对于线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的而是被反复使用的,此时就可能出现内存泄露

那么要怎么避免这个问题呢?

  1. 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据。因为它的 remove() 方法会主动将当前的 key 和 value(Entry) 进行清除
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBi4S9ko-1588755239799)(4. Java 并发(3)- ThreadLocal.assets/640.webp)]
    e.clear() 用于清除 Entry 的 key,它调用的是 WeakReference 中的方法:this.referent = null

    expungeStaleEntry(i) 用于清除 Entry 对应的 value

  2. ThreadLocal 的设计者也意识到了这一点(内存泄漏), 他们在一些方法中埋了对 key == null 的 value 擦除操作

    这里拿 ThreadLocal 提供的 get() 方法举例,它调用了 ThreadLocalMap.getEntry() 方法,对 key 进行了校验和对 null key 进行擦除
    在这里插入图片描述
    如果 key 为 null, 则会调用 getEntryAfterMiss() 方法,在这个方法中,如果 k == null , 则调用expungeStaleEntry(i) 方法用于清除 Entry 对应的 value

    expungeStaleEntry(i) 方法完成了对 key == null 的 key 所对应的 value 进行赋空, 释放了空间避免内存泄漏。

    同时它遍历下一个 key 为空的 Entry, 并将 value 赋值为 null, 等待下次 GC 释放掉其空间
    在这里插入图片描述
    同理,set() 方法最终也是调用该方法( expungeStaleEntry() ), 调用路径:

    set(T value) -> map.set(this, value) -> rehash() -> expungeStaleEntries()

    remove() 方法:

    remove() -> ThreadLocalMap.remove(this) -> expungeStaleEntry(i)

    当然这样做, 也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建 ThreadLocal 但不调用 set、get、remove 方法。所以最能解决问题的办法就是用完 ThreadLocal 后手动调用 remove()

1. 手动释放 ThreadLocal 遗留存储? 你怎么去设计/实现?

这里主要是强化一下手动 remove() 的思想和必要性,设计思想与连接池类似。

包装其父类 remove 方法为静态方法,如果是 Spring 项目, 可以借助于 bean 的声明周期, 在拦截器的afterCompletion 阶段进行调用

弱引用导致内存泄漏,那为什么 key 不设置为强引用?

这个问题就比较有深度了,是你谈薪的小小资本。

如果 key 设置为强引用, 当 threadLocal 实例释放后, threadLocal == null, 但是 threadLocal 会有强引用指向 threadLocalMap,threadLocalMap.Entry 又强引用 threadLocal, 这样会导致 threadLocal 不能正常被 GC 回收

弱引用虽然会引起内存泄漏, 但是也有 set、get、remove 方法操作对 null key 进行擦除的补救措施, 方案上略胜一筹。

线程执行结束后会不会自动清空 Entry 的 value ?

一并考察了你的 gc 基础

事实上,当 currentThread 执行结束后, threadLocalMap 变得不可达从而被回收,Entry 等也就都被回收了,但这个环境就要求不对 Thread 进行复用,但是我们项目中经常会复用线程来提高性能, 所以 currentThread 一般不会处于终止状态

2. Thread 和 ThreadLocal 有什么联系呢

Thread 和 ThreadLocal 是绑定的, ThreadLocal 依赖于 Thread 去执行, Thread 将需要隔离的数据存放到 ThreadLocal(准确的讲是 ThreadLocalMap)中, 来实现多线程处理

3. Spring 如何处理 Bean 多线程下的并发问题

ThreadLocal 天生为解决相同变量的访问冲突问题, 所以这个对于 Spring 的默认单例 bean 的多线程访问是一个完美的解决方案。Spring 也确实是用了 ThreadLocal 来处理多线程下相同变量并发的线程安全问题。

Spring 如何保证数据库事务在同一个连接下执行的?

要想实现 JDBC 事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。那 Spring 如何保证数据库事务在同一个连接下执行的呢?

DataSourceTransactionManager 是 Spring 的数据源事务管理器, 它会在你调用 getConnection() 的时候会从数据库连接池中获取一个 connection, 然后将其与 ThreadLocal 绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成

概要源码:

  1. 事务开始阶段

    org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin -> TransactionSynchronizationManager#bindResource -> org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aCnrV6L5-1588755239803)(4. Java 并发(3)- ThreadLocal.assets/1578447551461.png)]

  2. 事务结束阶段:

    org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion -> TransactionSynchronizationManager#unbindResource -> org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource -> TransactionSynchronizationManager#doUnbindResource
    在这里插入图片描述

4. 总结

  1. ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要;

  2. 每个 Thread 内部都有一个ThreadLocal.ThreadLocalMap 类型的成员变量,该成员变量用来存储实际的 ThreadLocal 变量副本;

  3. ThreadLocal 并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值