ThreadLocal学习笔记

对ThreadLocal的理解

ThreadLocal主要是用来存储线程内局部变量。每个线程都有自己的局部变量;这种变量在多线程环境下访问时(通过get或set方法访问)时,能保证各个线程里的变量独立于其他线程内变量之外。ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程相关联。

每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用).

可以总结为一句话: ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之前一些公共变量的传递的复杂度。

举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的。

有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

ThreadLocal 实现原理

ThreadLocal其实只是封装了对每个线程ThreadLocalMap成员变量的操作入口和一些基础API。数据并不存在ThreadLocal实例对象中,而是存储在各个线程自己的ThreadLocalMap成员变量中。 ThreadLocalMap是一种数据结构,有点像HashMap,底层是以Entry数组作为存储结构,可以保存“key:value”键值对,但是一个ThreadLocalMap只能保存一个key,而且key只能是关联的ThreadLocal实例对象。各个线程都有自己的ThreadLocalMap成员变量,各自保存各自的数据,所以各个线程的数据互不干扰(前提是 各自的数据 不是共享数据)。

ThreadLocal<String> threadLocalA = new ThreadLocal<>();
threadLocalA.set("张三");
// 张三
String name = threadLocalA.get();

ThreadLocal<String> threadLocalB = new ThreadLocal<>();
threadLocalB.set("李四");
// 李四
String name2 = threadLocalB.get();

image.png-234.8kB

每个Thread 维护一个 ThreadLocalMap 映射表,在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLocalMap中。这里需要注意的是,ThreadLocalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

ThreadLocal基础API

set方法

set方法用来设置当前线程的ThreadLocal的值。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 找到当前线程关联的ThreadLocals实例
    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);
}

通过上面的代码,我们首先知道ThreadLocal支持泛型,据说旧版本JDK这个API是不支持泛型的。

另外,说ThreadLocal使得各线程能够保存线程局部变量,并不是通过ThreadLocal.set()来实现的,数据也不是存储在ThreadLocal实例中的,而是通过让每个线程都拥有一个ThreadLocalMap实例对象,将数据存储在每个线程自己拥有的ThreadLocalMap实例对象中,从而做到线程隔离,线程局部变量的效果。

但是可以保存线程局部变量并不是说ThreadLocal可以用来解决共享对象的并发访问问题,一个共享对象,被多个线程当成线程局部变量存放到线程的ThreadLocalMap里,如果再有一个线程修改了这个共享对象,对其他线程都是能够感知到的。

还应该注意到,我们存放到ThreadLocalMap实例对象中的key都是this,也就是当前ThreadLocal实例对象。这能说明两点:第一点,一个线程只能往一个ThreadLocaL实例中存放一个value,多次存放,后面的value将会覆盖前面的value。第二点,如果ThreadLocal实例对象被多线程共享(比如定义成了static),则所有调用这个ThreadLocal实例对象set方法的线程中threadlocals成员变量中的key都是这个ThreadLocal实例对象。

get方法

用来获取当前线程存放在自己成员变量threadLocals中的值。

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);
    return value;
}

protected T initialValue() {
    return null;
}

通过上面的代码逻辑,我们知道获取当前线程存储在ThreadLocal实例里的值,其实就是获取当前线程threadLocals属性的value值。但是,如果当前线程的threadLocals为null(也就是之前没有进行过set操作),或者threadLocals属性中这value为null时,最终会调用initValue()方法;这个方法是protected的,所以我们知道该方法是可以被子类重载的。作用也就是当get不到时给出默认值,通常该方法都会以匿名内部类的形式被重载。比如:

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return 0;
        }
    };

remove方法

remove方法用于将当前线程的ThreadLocal绑定的值删除。在某些情况下,需要手动调用该方法,防止内存泄漏。

总结

每个Thread都拥有自己的ThreadLocalMap实例(该类是ThreadLocal的内部类),这个实例存储了以当前对象(this)为key,value是真正需要存储的对象。

好处:

线程局部变量高效保存:在常见的MVC三层架构中,如果我们想在多层使用同一个变量,我们简单的做法是将该变量作为参数往下传递,但是这样会加大层次之前的耦合力度。或者,我们可以new一个静态全局的大Map;将当前线程作为key,以实际需要存储的值作为value,put到这个大Map中;这样,当线程执行结束的时候,该线程所保存的线程局部量还强引用的保存在这个大Map中,浪费无用内存。ThreadLocal的实现,让线程局部变量交由具体线程自己管理,当进程执行结束的时候,由GC进行回收。

ThreadLocal实际应用场景

最常见的ThreadLocal使用场景为 用来解决数据库连接 Session管理等。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};
 
public static Connection getConnection() {
    return connectionHolder.get();
}

实际项目中的ThreadLocal使用

public class RequestThreadLocal {
	private static final ThreadLocal<HttpServletRequest> threadLocal = new ThreadLocal<HttpServletRequest>();
	private static final ThreadLocal<List<String>> historyThreadLocal = new ThreadLocal<List<String>>();
	private static final Log log = LogFactory.getLog(RequestThreadLocal.class);
	
	public static HttpServletRequest get(){
		return threadLocal.get();
	}
	
	public static void set(HttpServletRequest request){
		log.debug("Put the pagination object into thread local");
		threadLocal.set(request);
	}
	public static void remove(){
		log.debug("clear the pagination object from thread local which related with current thread.");
		threadLocal.set(null);
	}
}

//调用
@Controller
public class ControllerFilter implements Filter{
	@Override
	public void destroy() {}

	@Override
	public void doFilter(ServletRequest req, ServletResponse res,
			FilterChain chain) throws IOException, ServletException {
			RequestThreadLocal.set((HttpServletRequest) req);
			chain.doFilter(req, res);
			RequestThreadLocal.remove();
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {}
}

//调用
/**
 * 获取当前登录用户
 * @return
 */
public static AclUserDto getLoginUser(){
	 HttpSession session = RequestThreadLocal.get().getSession();
     return (AclUserDto) session.getAttribute(LoginInfo.SESSIONUSER.getDesc());
}

使用ThreadLocal实现简单的读写分离

@Component
@Aspect
public class DataSourceMethodInterceptor {

    @Before("execution(* com.xxx.xxx.xxx.xxx.service.impl.*.*(..))")
    public void dynamicSetDataSoruce(JoinPoint joinPoint) throws Exception {
        String methodName = joinPoint.getSignature().getName();
        // 查询读从库
        if (methodName.startsWith("select") || methodName.startsWith("load") || methodName.startsWith("get") || methodName.startsWith("count") || methodName.startsWith("is")) {
            DynamicDataSourceHolder.setDataSource("slave");
        } else { // 其他读主库
            DynamicDataSourceHolder.setDataSource("master");
        }
    }

}

总结

  • 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程私有的值保存到其中,各个线程互不干扰。

  • ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

参考文章

原文地址:https://www.cnblogs.com/boothsun/p/6519323.html