ThreadLocal
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).
上文节选自ThreadLocal
的 JavaDoc,从描述的内容上看我们就可以看出 ThreadLocal
的作用是提供线程局部变量,这些局部变量是原变量的副本;ThreadLocal
是为每个线程提供一份变量的副本,由此不会影响到其他线程的变量副本。
源码浅析
我们先来看看 ThreadLocal
下支持的几个常用操作。set(T value)
,get()
,remove()
;
set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从源代码可以看出,set
操作先获取当前线程,再获取当前线程的 ThreadLocalMap
,如果 map
不为空则把当前 ThreadLocal
对象实例作为key,传进来的value作为值,否则创建一个map,再按照键值对放进去。从这里可以看出,实质上我们最后的存储介质就是这个ThreadLocalMap
,那这个ThreadLocalMap
是什么呢?接着往下看。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
==================================================================
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
...
}
每个Thread
对象都维护一个ThreadLocalMap
类型的变量 threadLocals
;这个ThreadLocalMap
是 ThreadLocal
的一个内部类, ThreadLocalMap
内部维护了一个Entry
(键值对),这里可以看到 Entry
继承了 WeakReference
,其对应的构造方法里 ,key值调用了父类的方法,那么意味着Entry
所对应的key(ThreadLocal对象实例)的引用是一个弱引用。
WeakReference
是java四种引用用中的弱引用,当有gc发生时就会被回收。
还有其他三种分别是强引用,虚引用,软引用。
那为什么这里设置成弱引用呢?主要是为了防止内存泄漏,下面我们来分析一下。
假设我们在方法内部new
了一个ThreadLocal
对象,并往里面set
值。此时堆内存中的 ThreadLocal
对象有两块引用指向它,第一个引用是栈内存的强引用;另外一个是当set
值之后Entry
的key作为的弱引用。方法结束时,当指向 ThreadLocal
对象的强引用消失时,对应的弱引用也会自动被回收。
我们假设Entry
中的引用是强引用,当指向ThreadLocal
对象的强引用消失时,ThreadLocal
对象应该被回收但却因为Entry
中的强引用无法回收,我们知道 ThreadlocalMap
是属于Thread
的,如果是服务端线程的话,因为Thread
长期存在,ThreadLocalMap
也必然长期存在,那么对应的这个Entry
也会长期存在,那这个ThreadLocal
对象就不会被回收,就造成了内存泄漏。所以这就是为什么要使用弱引用的原因。
除此之外,JDK的设计者已经帮我们使用弱引用来避免了内存泄漏,仔细想想这样就不会再产生内存泄漏了吗?当对应的Entry
中的key被回收,那这个value是不是就无法获取到了呢,但由于Thread
长期存在,ThreadLocalMap
也长期存在,Entry
也会长期存在,value也会永远都无法释放了,这样还是会造成内存泄漏。所以,在每次使用完之后,都需要调用remove
方法进行资源清除。
说到这里,回想一下如果在拦截器里使用ThreadLocal,就能理解为什么在afterCompletion
需要remove
方法了吧,如果不进行资源清除,就会导致线程在第二次请求中get
到第一次请求的set
进去的值。
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
remove
操作还是比较简单的,就是通过当前Thread
对象获取 ThreadLocalMap
,若不为空则再根据 ThreadLocal
对象作为key删除value。
get()
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal>) this);
}
return value;
}
接着我们来看下get
操作,get
操作同样也是先获取当前Thread
的ThreadLocalMap
,再根据ThreadLocal
对象获取对应的Entry
,最后获取值。若map为空,则初始化值然后将初始化的值返回。
应用实践
Spring事务管理
在Web项目编程中,我们都会与数据库进行打交道,往往通常的做法是一个Service层里包含了多个Dao层的操作,要保证Service层操作的原子性,就要保证这些Dao操作是在同一个事务里,在同一个事务里就要确保多个Dao层的操作都是同一个Connection,那如何保证呢?我们可以确定的是该多个Dao层的操作都是由相同的线程进行处理的,那只要把Connection与线程绑定就可以了,所以Spring这里就巧妙的使用ThreadLocal
来解决了这个问题。
Spring中有一个类 DataSourceUtils
其中有方法是获取数据源的Connection的,里面有一个getConnection
方法,如下所示。
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
catch (IllegalStateException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage());
}
}
for example when using {@link DataSourceTransactionManager}. Will bind a Connection to the
thread if transaction synchronization is active
这里的注释有一个细节要关注到就是注释中提及到 如果使用数据源事务管理器,当开启事务时,那么就会绑定连接到当前线程。
/**
* Actually obtain a JDBC Connection from the given DataSource.
* Same as {@link #getConnection}, but throwing the original SQLException.
* Is aware of a corresponding Connection bound to the current thread, for example
* when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread
* if transaction synchronization is active (e.g. if in a JTA transaction).
*
Directly accessed by {@link TransactionAwareDataSourceProxy}.
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource
* @throws SQLException if thrown by JDBC methods
* @see #doReleaseConnection
*/
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(con, dataSource);
throw ex;
}
}
return con;
}
从源代码大致可以看出首先从TransactionSynchronizationManager
中获取ConnectionHolder
,若存在则直接返回Connection
,若不存在则新生成一个Connection
并装到ConnectionHolder
中然后注册到TransactionSynchronizationManager
中,然后再返回Connection
。由此,我们可以看出TransactionSynchronizationManager
在这其中起到了管理Connection
的作用。
接着看下TransactionSynchronizationManager
类。其中getResource
方法和bindResource
方法都在上面的doGetConnection
方法中有过调用,那我们就注重看下这几个方法。
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal
从以上代码可以得出TransactionSynchronizationManager
类维护了ThreadLocal
对象来进行资源的存储,包括事务资源(Spring中对JDBC的Connection或Hibernate的Session都称之为资源),事务隔离级别等。
名为resources变量的ThreadLocal
对象存储的是DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
再结合DataSourceUtils
的doGetConnection
方法和TransactionSynchronizationManager
的bindResource
和getResource
方法可知:在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey和value值为DataSource获得的Connection
对象封装后的ConnectionHolder
。等这个线程下一次再次访问中就能保证使用的是第一次创建的ConnectionHolder
中的Connection
对象。
参考链接
【死磕Java并发】—–深入分析ThreadLocal
Spring事务之如何保证同一个Connection对象
Spring是如何保证同一事务获取同一个Connection的?使用Spring的事务同步机制解决:数据库刚插入的记录却查询不到的问题【享学Spring】