ThreadLocal
1、ThreadLocal的使用场景
ThreadLocal主要用来存储线程的本地数据,做到线程数据的隔离。常用的方法有:set()
、get()
、remove()
分别对应存储、获取和删除。下面看几个使用场景:
场景1:在拦截器中的使用
//将其封装在工具类中 public class ContextUtils { public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal(); }
//执行前 存储 public boolean pretHandle(HttpServletRequest request) { //解析token获取用户信息 String token = request.getHeader("token"); UserInfo userInfo = parseToken(token); //存入 ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo); return true; } //执行后 删除 public void postHandle(HttpServletRequest request) { ContextUtils.USER_INFO_THREAD_LOCAL.remove(); }
使用
//提交订单 public void orderSubmit(){ //获取用户信息 UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get(); //下单 submit(userInfo); //删除购物车 removeCard(userInfo); }
场景2:时间格式化类 SimpleDateFormat,它是线程不安全的,多线程下使用同一个实例会出现数据错误,每次使用都创建又比较浪费内存。
public class DateFormatThreadLocal { private static final ThreadLocal<SimpleDateFormat> threadLocalFormatter = ThreadLocal.withInitial(() -> { // 初始化 SimpleDateFormat 实例 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); }); public static Date parseDate(String dateStr) throws ParseException { SimpleDateFormat formatter = threadLocalFormatter.get(); return formatter.parse(dateStr); } public static String formatDate(Date date) { SimpleDateFormat formatter = threadLocalFormatter.get(); return formatter.format(date); } }
场景3:数据库读写分离,利用ThreadLocal保存数据源标识,使同一个线程使用同一个数据源。
public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); } public static String getDataSourceType() { return contextHolder.get(); } public static void clearDataSourceType() { contextHolder.remove(); } } public class DataSourceRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceType(); } } // 使用DataSourceContextHolder设置数据源类型 DataSourceContextHolder.setDataSourceType("write"); // 设置为写数据源 // 执行数据库写操作 DataSourceContextHolder.setDataSourceType("read"); // 设置为读数据源 // 执行数据库读操作 DataSourceContextHolder.clearDataSourceType(); // 清除数据源类型
DataSourceContextHolder
类使用ThreadLocal
保存当前线程所需的数据源类型。DataSourceRoutingDataSource
类继承自AbstractRoutingDataSource
,并重写了determineCurrentLookupKey
方法,该方法根据DataSourceContextHolder
保存的数据源类型来决定最终使用哪个数据源。这样,在同一线程内,无论执行读操作还是写操作,都能保证使用正确的数据源。
2、ThreadLocal 原理
这是ThreadLocal中两个内部类关系定义:
public class ThreadLocal<T> {
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的定义:
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; }
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里ThreadLocalMap内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用 Entry(k,v) 来进行存储。
ThreadLocal的set()方法实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象;ThreadLocal的get()方法实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
3、ThreadLocal 使用弱引用以及内存泄漏问题
了解了他们之间的定义关系之后,我们再来看一下ThreadLocal实例在线程中的引用关系:
看到这个引用关系我们就知道,Entry有两个引用关系,一个是与value的引用,一个是与key—ThreadLocal实例的引用,其中与ThreadLocal实例的应用为弱引用。
这个弱引用,保证当Entry没有被清除的情况下,ThreadLocal也能被回收,但是如果不手动清理调value的话,也可能造成内存泄漏,因为ThreadLocalMap的生命周期一致。所以导致内存泄漏的根源是:没有手动清除Entry与value的引用关系。
但是ThreadLocal的 set/get/remove 也有判断当 threadLocal 作为key为null 时,也会自动删除对应的value。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
ThreadLocal的原理、使用注意的地方(可能会导致内存泄漏)
参考文章:1、https://www.cnblogs.com/zxporz/p/10900852.html
2、https://zhuanlan.zhihu.com/p/102571059