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<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
        }
    }
...    
}

每个Thread对象都维护一个ThreadLocalMap 类型的变量 threadLocals ;这个ThreadLocalMapThreadLocal 的一个内部类, ThreadLocalMap 内部维护了一个Entry (键值对),这里可以看到 Entry 继承了 WeakReference ,其对应的构造方法里 ,key值调用了父类的方法,那么意味着Entry 所对应的key(ThreadLocal对象实例)的引用是一个弱引用。

WeakReference 是java四种引用用中的弱引用,当有gc发生时就会被回收。
还有其他三种分别是强引用,虚引用,软引用。

那为什么这里设置成弱引用呢?主要是为了防止内存泄漏,下面我们来分析一下。

image.png
假设我们在方法内部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 操作同样也是先获取当前ThreadThreadLocalMap,再根据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.
* <p>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).
* <p>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<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");
    
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");

    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<>("Current transaction read-only status");

    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<>("Current transaction isolation level");

    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<>("Actual transaction active");
...
public static Object getResource(Object key) {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    Object value = doGetResource(actualKey);
    if (value != null && logger.isTraceEnabled()) {
        logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
                     Thread.currentThread().getName() + "]");
    }
    return value;
}
    
private static Object doGetResource(Object actualKey) {
    Map<Object, Object> map = resources.get();
    if (map == null) {
        return null;
    }
    Object value = map.get(actualKey);
    // Transparently remove ResourceHolder that was marked as void...
    if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
        map.remove(actualKey);
        // Remove entire ThreadLocal if empty...
        if (map.isEmpty()) {
            resources.remove();
        }
        value = null;
    }
    return value;
}
...
public static void bindResource(Object key, Object value) throws IllegalStateException {
    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
    if (map == null) {
        map = new HashMap<>();
        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() + "]");
    }
}

从以上代码可以得出TransactionSynchronizationManager 类维护了ThreadLocal对象来进行资源的存储,包括事务资源(Spring中对JDBC的Connection或Hibernate的Session都称之为资源),事务隔离级别等。
名为resources变量的ThreadLocal对象存储的是DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
再结合DataSourceUtilsdoGetConnection方法和TransactionSynchronizationManagerbindResourcegetResource方法可知:在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey和value值为DataSource获得的Connection对象封装后的ConnectionHolder。等这个线程下一次再次访问中就能保证使用的是第一次创建的ConnectionHolder中的Connection对象。

参考链接

【死磕Java并发】—–深入分析ThreadLocal
Spring事务之如何保证同一个Connection对象
Spring是如何保证同一事务获取同一个Connection的?使用Spring的事务同步机制解决:数据库刚插入的记录却查询不到的问题【享学Spring】


Ekko
2 声望0 粉丝