一、ThreadLocal简介
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
下图可以增强理解:
二、ThreadLocal原理
ThreadLocal变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()方法说起:
public T get() {//获得当前线程Thread t = Thread.currentThread();//每个线程 都有一个自己的ThreadLocalMap,//ThreadLocalMap里就保存着所有的ThreadLocal变量ThreadLocalMap map = getMap(t);if (map != null) {//ThreadLocalMap的key就是当前ThreadLocal对象实例,//多个ThreadLocal变量都是放在这个map中的ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//从map里取出来的值就是我们需要的这个ThreadLocal变量T result = (T)e.value;return result;}}// 如果map没有初始化,那么在这里初始化一下return setInitialValue();
}
可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//key就是一个弱引用Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露。
三、ThreadLocal中的内存泄漏问题
虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:
ThreadLocalMap在内存中的关系如下图:
可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:
以getEntry()为例:
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)//如果找到key,直接返回return e;else//如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来return getEntryAfterMiss(key, i, e);
}
下面是getEntryAfterMiss()的实现:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {// 整个e是entry ,也就是一个弱引用ThreadLocal<?> k = e.get();//如果找到了,就返回if (k == key)return e;if (k == null)//如果key为null,说明弱引用已经被回收了//那么就要在这里回收里面的value了expungeStaleEntry(i);else//如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entryi = nextIndex(i, len);e = tab[i];}return null;
}
真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:
从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。
但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。
比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。
因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。
四、ThreadLocal的使用
(一)简单使用
假设循环创建1000个线程,每个线程都用当前次循环的i值去调用say方法,方法中把对应参数值赋给一个静态变量num,然后还让线程sleep1毫秒模拟执行复杂业务逻辑,然后再输出num,同时往threadlocal中放入一开始的num。
public class School {static ThreadLocal<String> threadLocal = new ThreadLocal<>();static int num ;static void say(int time) throws InterruptedException {num = time;threadLocal.set(num+"");Thread.sleep(1);System.out.println("从ThreadLocal拿出来的:" + threadLocal.get());System.out.println("从当前方法的静态变量中的:"+num);}public static void main(String[] args) {for (int i = 0; i < 1000; i++) {// 线程创建int finalI = i;Thread t = new Thread(new Runnable() {@Overridepublic void run() {try {say(finalI);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}}
}
查看结果:
不难看出从当前方法的静态变量中拿出来打印的很多重复值,发生了典型的值覆盖的线程安全问题,而从ThreadLocal中取出来的就是保证了没有重复值,这就是其中一个应用场景,典型的这个线程不安全的场景就有大名鼎鼎的日期格式化工具simpledateformat,多线程传进去的time会被覆盖。
(二)复杂使用
我们先看JdbcTemplate类数据访问类
public Object execute(ConnectionCallback action)throws DataAccessException{Assert.notNull(action, "Callback object must not be null");Connection con = DataSourceUtils.getConnection(getDataSource());try {Connection conToUse = con;if (this.nativeJdbcExtractor != null){conToUse = this.nativeJdbcExtractor.getNativeConnection(con);}else{conToUse = createConnectionProxy(con);}localObject1 = action.doInConnection(conToUse);}catch (SQLException ex){Object localObject1;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);}finally {DataSourceUtils.releaseConnection(con, getDataSource());}}
由上述源码中Connection con = DataSourceUtils.getConnection(getDataSource());这个可以看出,DataSourceUtils类保证当前线程获得的是同一个Connection对象。下面我们主要分析DataSourceUtils类:
public static Connection getConnection(DataSource dataSource)throws CannotGetJdbcConnectionException{try{return doGetConnection(dataSource);} catch (SQLException ex) {}throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);}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(dataSource.getConnection());}return conHolder.getConnection();}logger.debug("Fetching JDBC Connection from DataSource");Connection con = dataSource.getConnection();if (TransactionSynchronizationManager.isSynchronizationActive()) {logger.debug("Registering transaction synchronization for JDBC Connection");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);}}return con;}
由以上源码可以知道,数据库连接从TransactionSynchronizationManager中获得,如果已经存在则获得,否则重新从DataSource创建一个连接,并把这个连接封装为ConnectionHolder,然后注册绑定到TransactionSynchronizationManager中,并返回Connection对象。同时,可以看出DataSource和ConnectionHolder的存储管理在TransactionSynchronizationManager中,继续分析TransactionSynchronizationManager中的关键代码:
private static final ThreadLocal resources = new NamedThreadLocal("Transactional resources");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 map = (Map)resources.get();if (map == null) {return null;}Object value = map.get(actualKey);if (((value instanceof ResourceHolder)) && (((ResourceHolder)value).isVoid())) {map.remove(actualKey);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 map = (Map)resources.get();if (map == null) {map = new HashMap();resources.set(map);}if (map.put(actualKey, value) != null) {throw new IllegalStateException("Already value [" + map.get(actualKey) + "] 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() + "]");}
分析源码可以得出,
(1)TransactionSynchronizationManager内部用ThreadLocal对象存储资源,ThreadLocal存储的为DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
(2)结合DataSourceUtils的doGetConnection函数和TransactionSynchronizationManager的bindResource函数可知:在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey【Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);】value值为DataSource获得的Connection对象封装后的ConnectionHolder。等这个线程下一次再次访问中就能保证使用的是第一次创建的ConnectionHolder中的Connection对象。
当事务结束后,调用【DataSourceUtils.releaseConnection(con, getDataSource());】将ConnectionHolder从TransactionSynchronizationManager中解除。当谁都不用,这个连接被close。
public static void releaseConnection(Connection con, DataSource dataSource){try{doReleaseConnection(con, dataSource);}catch (SQLException ex) {logger.debug("Could not close JDBC Connection", ex);}catch (Throwable ex) {logger.debug("Unexpected exception on closing JDBC Connection", ex);}}public static void doReleaseConnection(Connection con, DataSource dataSource)throws SQLException{if (con == null) {return;}if (dataSource != null) {ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);if ((conHolder != null) && (connectionEquals(conHolder, con))){conHolder.released();return;}}if ((!(dataSource instanceof SmartDataSource)) || (((SmartDataSource)dataSource).shouldClose(con))) {logger.debug("Returning JDBC Connection to DataSource");con.close();}}
参考文章:
ThreadLocal使用与原理 - 知乎
史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客
ThreadLocal从简单使用及源码_threadlocal代码使用_凉拌海蜇丝的博客-CSDN博客
Spring事务之如何保证同一个Connection对象_HaiwiSong的博客-CSDN博客