今天想梳理一个常见的面试题。在开始之前,让我们一起来回顾一下昨天的那篇文章——《Spring 事务原理总结七》。这篇文章比较啰嗦,层次也不太清晰,所以以后有机会我一定要重新整理一番。这篇文章主要想表达这样一个观点:Spring的嵌套事务是通过ThreadLocal实现的。当一个请求从浏览器发送到后台以后,后台会启动一个线程去处理这个请求,在处理的过程中如果用到了一个需要事务的方法A,而A又调用了另一个需要事务的方法B,那么这时就发生了事务嵌套(同时还涉及到了事务传播行为,即事务A与事务B之间的相互关系)。为了解决事务嵌套的问题,Spring又定义了一个记录与事务相关的信息的类TransactionInfo。这样A就有了一个对应的TransactionInfo对象,B也有了一个对应的TransactionInfo对象。而A调用了B,所以A和B对应的TransactionInfo对象之间就有了关联关系。在TransactionInfo类中有一个oldTransactionInfo属性,这个记录的就是老的事务对象。看过前面系列文章又好好思考过的童鞋可能已经明白了,在A中通过代理调用B的时候,此时两个事务位于同一线程中,接着将A对应的TransactionInfo对象赋值给B对应的TransactionInfo对象的oldTransactionInfo属性,然后将B对应的TransactionInfo对象绑定到ThreadLocal,接着待B执行完后,将当前线程绑定的TransactionInfo还原为A对应的TransactionInfo,此时就完成了嵌套事务之间信息传递。具体参见下面这幅图:
上面这幅图并不完美,但将前面一段的描述图形化,这应该会好理解一点吧!在这段回顾中,我们不断提到ThreadLocal,那它究竟是干什么的?原理又是什么呢?想必大家也都听说过,ThreadLocal存在内存泄漏的风险,这又是咋回事呢?
概述
ThreadLocal,即线程本地变量,是Java中用于提供线程局部存储的类,位于java.lang包下。它的核心原理在于为每个使用ThreadLocal的线程创建一个独立的副本,使得每个线程在访问ThreadLocal时获取到的是自己线程内的私有数据,而不是共享同一份数据,从而避免了多线程间的同步问题。其工作机制是这样的:
- 内部存储结构:ThreadLocal内部维护了一个Map数据结构,其键是当前的线程对象(ThreadLocal.ThreadLocalMap),值是我们想要隔离存储的对象实例。这意味着每个线程都有一个独立的ThreadLocalMap来保存自己的ThreadLocal副本。
- 存取操作:
- set(T value)方法允许我们设置线程局部变量的值。它会将给定的值与当前执行线程关联起来,在该线程的ThreadLocalMap中存储
- get()方法则返回当前线程所对应的ThreadLocal变量的副本值。如果该线程尚未初始化,则可能返回默认值或抛出异常
- remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
- 线程生命周期管理:当线程结束生命周期后,其内部的ThreadLocalMap应该被清理以释放资源。然而,如果线程不再使用某个ThreadLocal但未手动删除引用,可能会导致内存泄漏,因为ThreadLocalMap中的Entry不会自动移除。
- 弱引用与内存泄漏:在内部实现上,ThreadLocalMap的条目是通过弱引用(WeakReference)指向ThreadLocal实例的,这意味着只有当没有强引用指向ThreadLocal实例时,这些条目才能被垃圾回收器回收。但如果仅ThreadLocalMap中的条目持有目标对象的唯一引用,即使线程已经完成任务,由于弱引用的存在,目标对象也不会立即被回收,这可能导致无用对象占据内存空间,直到下次JVM进行垃圾回收周期时才可能清除。
- 使用场景:ThreadLocal常用于需要在线程间隔离状态信息的情况,例如数据库连接、事务上下文、用户身份信息等,尤其是在处理每个线程都需要独立的实例且不希望影响其他线程的情况下。
总的来说,ThreadLocal通过为每个线程维护一份独立的数据副本,巧妙地实现了多线程环境下的数据隔离和安全性,并简化了代码的编写,减少了同步块或锁的使用。但它也要求开发者关注并正确管理其生命周期,以免引发内存泄露问题。