驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
ThreadLocal自我总结(一)
/    

ThreadLocal自我总结(一)

开篇

ThreadLocal这个东西复杂吗?在我看来,对于初学者而言,还是有些难度的,其内部的存储结构挺饶的,需要沉下心来多刷刷别人的博客,然后自己根据源码、测试以及个人思考慢慢领会。

本文作为抛石引玉,给不清楚的同学给点灵感,也请大神们不吝赐教。

本系列从如下几个知识点进行分析:

  • ThreadLocal是什么

  • ThreadLocal使用场景有哪些?我喜欢通过实际的案例来引入,学有所用,这样才能学得快。

备注:知识这种东西,对于普通人来说,第一次接触很多东西都不好消化,但是如果你有恒心和毅力,多看看、多想想,困难的事情就会慢慢变得简单。

ThreadLocal 是什么

ThreadLocal类用来提供线程内部的局部变量。

这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。

所以说:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

使用场景

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

比如下面的实际应用场景:在一个管理系统中,需要在 Service 层记录当前进行操作的用户的 唯一标识。这个唯一标识从根本来看是保存在浏览器的 cookie 中的,我在 Controller 层不管通过 HttpServletRequest还是其他手段(这里展开讲,又可以写个博文了,所以就此打住)都可以获取到这个唯一标识。

如果Service 层进行操作的时候,也需要这个唯一标识,其中一个不优雅但是很实用的办法就是,Service 层每一个方法都有一个形参来让 Controller 层调用 Service 层的时候,将这个唯一的标识传递过去,代码如下所示:

// Controller 层代码
public Result modifyPerson(Person person,String userId){}

// Service 层的代码
public boolean modifyPerson(Person person,String userId){}
public boolean deletePerson(Person person,String userId){}
public List<Person> findPerson(Person person,String userId){}

在上述Service层代码中,其实 userId 的作用仅仅是在输出 log 日志的时候,将当前操作的用户打印出来和实际的业务是没有任何关系的。

大家觉得这样的设计是不是很坑爹 ▄█▀█●

那么有没有办法,不显式的传递userId但是却可以从某个地方获取到这个userId了?

答案肯定是有的,可以利用ThreadLocal 从字面意思来理解这就是一个线程本地变量,可以简单理解为 将变量存储到当前线程中。那么只要还在这个线程中,肯定有办法获取到这个变量。

那么如何使用了?

如何使用

举例一

我给出一个参考做法,直接上代码:

public class MicLocalContext {

    /**
     * 局部变量
     */
    private static final ThreadLocal<CmdBO> CMD_BO_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 设置全局变量
     *
     * @param cmdBO 关键信息
     */
    public static void setLocalContext(CmdBO cmdBO) {
        CMD_BO_THREAD_LOCAL.set(cmdBO);
    }

    /**
     * 获取全局的 CmdBo 变量
     *
     * @return CmdBo 变量
     */
    public static CmdBO getLocalContext() {
        return CMD_BO_THREAD_LOCAL.get();
    }

    /**
     * 重置 CmdBo 变量<br>
     * <Strong>一定要记得调用</Strong>
     */
    public static void restLocalContext() {
        CMD_BO_THREAD_LOCAL.remove();
    }
}

上述代码还是比较简单的:

  • 首先定义一个入口类MicLocalContext,在其中定义一个private static final 的变量。
  • 然后定义公共的getsetclear的等方法来进行对应ThreadLocal的操作。

定义好后,可以在业务的某个开头的部分,将需要在本次线程共享的变量放入ThreadLocal中。

MicLocalContext.setLocalContext(cmdBO);

当需要在这个线程中使用的时候,可以通过:

CmdBO bo = MicLocalContext.getLocalContext();

基本的用法就这个简单,不过请一定记住,在业务的最后调用 restLocalContext()清空这个变量,这样可以有效的防止内存泄露。

举例二

基本使用

这里补充句,其实 ThreadLocal在 SpringMVC 中有使用到,很多时候我们没有感知到而已,比如说:我们需要在 Service 层获取 HttpServeletRequest 我们可以采取如下办法:

先在 web.xml定义一个 listner

<listener>
	<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
 </listener>

然后在项目中就可以通过如下代码进行使用:

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

我们来分析下这个底层的实现,通过这些实现,我们来分析我们需要注意什么!

源码分析

public class RequestContextListener implements ServletRequestListener {
	
	@Override
	public void requestInitialized(ServletRequestEvent requestEvent) {
	   // 省略....
		LocaleContextHolder.setLocale(request.getLocale());
		RequestContextHolder.setRequestAttributes(attributes);
	}

	@Override
	public void requestDestroyed(ServletRequestEvent requestEvent) {
		// 省略....
		if (threadAttributes != null) {
			LocaleContextHolder.resetLocaleContext();
			RequestContextHolder.resetRequestAttributes();
        }
        // 省略....
	}

}
  • 首先该类实现了一个接口ServletRequestListener 从名字可以猜出是监听 Servlet 初始化,从其要求实现的方法来看,确实如此。
  • requestInitialized 中,通过LocaleContextHolderRequestContextHolder设置了当前线程共享的变量。
  • requestDestroyed 中通过resetLocaleContext 进行了 clear.【这里请思考为什么要 clear,不 clear 有问题吗?】

那么我们继续看一个 LocaleContextHolder的实现,对比上面的MicLocalContext是不是很眼熟了!

public abstract class LocaleContextHolder {
    
    private static final ThreadLocal<LocaleContext> localeContextHolder = new NamedThreadLocal("LocaleContext");
    
    private static final ThreadLocal<LocaleContext> inheritableLocaleContextHolder = new NamedInheritableThreadLocal("LocaleContext");

    public LocaleContextHolder() {
    }

    public static void resetLocaleContext() {
        localeContextHolder.remove();
        inheritableLocaleContextHolder.remove();
    }

    public static void setLocaleContext(LocaleContext localeContext) {
        setLocaleContext(localeContext, false);
    }

    public static LocaleContext getLocaleContext() {
        return localeContext;
    }

其实这种用法在 springmvc 中非常常见,如果你乐于发现,乐于思考,会看到类似代码在很多地方有使用。

使用总结

  1. 定义一个XxxHolder 来作为一个容器,存放 ThreadLocal
  2. 定义一个private static final ThreadLocal<T> 用来存放ThreadLocal
  3. 定义3个方法:get set remove

就这么简单~~~

骐骥一跃,不能十步。驽马十驾,功在不舍。