文章目录
参考文献:
手撕面试题ThreadLocal!!!
ThreadLocal-面试必问深度解析
TransmittableThreadLocal,父子线程之间的变量处理
实际应用场景
场景一:代替参数的显式传递
应用实列
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
这个场景其实使用的比较少,一方面显式传参比较容易理解,另一方面我们可以将多个参数封装为对象去传递。
场景二:全局存储用户信息
原博文
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。 这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。
在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)
说明 : 简单说下我写这篇文章的来由吧,最近准备新开一个项目前后端分离的,后端通过springboot实现,前段就不说了,那么就会考虑到用户登录成功以后登录信息保存在什么地方,是通过前后端一直传递参数么,那就蛋疼了。通过session存储信息。。那不是要每次都要在视图层获取session在一层一层的传递下去。。想了想通过ThreadLocal来存储用户信息吧,这样就可以直接在dao层调用了。说干就干、走起。
1 建立ThreadLocal实体类
/**
* @author : lqf
* @description : 用于存储用户信息
* @date : Create in 10:47 2018/6/04
*/
public class AgentThreadLocal {
private AgentThreadLocal(){
}
//ConsoleUserVo是存储用户信息的实体类我就不说了
private static final ThreadLocal<ConsoleUserVo> LOCAL = new ThreadLocal<ConsoleUserVo>();
public static void set(ConsoleUserVo user){
LOCAL.set(user);
}
public static ConsoleUserVo get(){
return LOCAL.get();
}
public static void remove(){
LOCAL.remove();
}
}
由于每一次请求都是一个独立的线程,ThreadLocal中的变量需要我们通过session做一个中专的配置,每次请求都判断这个session中是否存在用户信息,如果session中存在用户信息就将用户信息保存到ThreadLocal中,下面上代码
2 创建sessionFilter
/**
* @author : lqf
* @description :
* @date : Create in 13:12 2018/6/26
*/
public class SessionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
ConsoleUserVo userSession = (ConsoleUserVo)request.getSession().getAttribute("userInfo");
if (userSession != null) {
//先销毁在添加否则触发不了监听器中的attributeAdded
request.getSession().removeAttribute("userInfo");
//重新设值session
request.getSession().setAttribute("userInfo", userSession);
}
chain.doFilter(req, res);
}
@Override
public void destroy() {
}
}
上面代码中的销毁session是个坑,经测试如果session的key相同value也相同这个时候重新给session赋值是不会触发session监听器的创建和替换方法的,下面看下session监听
/**
* @author : lqf
* @description :
* @date : Create in 15:17 2018/6/26
*/
@WebListener
public class SessionAttributeListener implements HttpSessionAttributeListener {
@Override
//创建session时触发
public void attributeAdded(HttpSessionBindingEvent event) {
if ("userInfo".equals(event.getName())) {
AgentThreadLocal.set((ConsoleUserVo) event.getValue());
}
}
@Override
//销毁session时触发
public void attributeRemoved(HttpSessionBindingEvent event) {
if ("userInfo".equals(event.getName())) {
AgentThreadLocal.remove();
}
}
@Override
//替换session时触发
public void attributeReplaced(HttpSessionBindingEvent event) {
if ("userInfo".equals(event.getName())) {
AgentThreadLocal.set((ConsoleUserVo) event.getValue());
}
}
}
springboot启动类中创建filter实例
@ServletComponentScan
@SpringBootApplication
public class AgentApiApplication {
private static Logger LOG = LoggerFactory.getLogger(AgentApiApplication.class);
public static void main(String[] args) {
SpringApplication.run(AgentApiApplication.class, args);
}
@Bean
public FilterRegistrationBean jwtFilterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
HttpBearerAuthorizeAttribute httpBearerFilter = new HttpBearerAuthorizeAttribute();
registrationBean.setFilter(httpBearerFilter);
registrationBean.setOrder(Integer.MAX_VALUE);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
@Bean
public FilterRegistrationBean sessionFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
SessionFilter sessionFilter = new SessionFilter();
registrationBean.setFilter(sessionFilter);
registrationBean.setOrder(Integer.MIN_VALUE);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
这里需要注意的是启动类上需要加上@ServletComponentScan注解,你们可能会说你的启动类中为什么两个filter啊,这是我特意加上的,在我们的项目实战中可能不只一个filter有可能是3个5个的对吧,这个时候我们就要定义这些filter的执行顺序了对吧。可能有的人会说博主你这有些麻烦啊!直接通过注解来实现呗@Order(Integer.MAX_VALUE) //执行排序值越小越先执行
@WebFilter(filterName = “sessionFilter”, urlPatterns = “/*”)
这里有个坑我现在没有找到他的错误原因,我使用的springboot1.5通过注解定义的排序顺序不生效,通过手动常见的bean是能正常生效的。你们如果找到这个问题共享一下啊,谢了
回归正题
//登录成功
ConsoleUserVo userVo = new ConsoleUserVo();
userVo.setUserName("");
userVo.setAge("");
//将用户信息存储在session中这个时候就会调用session监听器的创建方法
request.getSession().setAttribute("userInfo", userVo);
//第二遍给已经存储的key付相同的值是不能调用session监听中的任何方法的
request.getSession().setAttribute("userInfo", userVo);
//第三遍存储session key是已经存在的但是值变了,这个时候是可以触发监听的替换方法的
request.getSession().setAttribute("userInfo", "123123");
上面的session存储纯粹是为了告诉大家什么时候触发监听,正常逻辑中只写一个存储就可以了,通过这个存储触发大家就能明白上面的sessionFilter中为什么要删除session,在进行存储了。
到这里大家就可以再任一方法中进行
AgentThreadLocal .get()方法了。
场景三:解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?
在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题
ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数
场景四:多数据源的配置
慎用的场景
1.线程池中线程调用使用ThreadLocal 由于线程池中对线程管理都是采用线程复用的方法。在线程池中线程非常难结束甚至于永远不会结束。 这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致
2.异步程序中,ThreadLocal的参数传递是不靠谱的, 由于线程将请求发送后。就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是其他的线程。Java8中的并发流也要考虑这种情况
3.使用完ThreadLocal ,最好手动调用 remove() 方法,防止出现内存溢出,因为中使用的key为ThreadLocal的弱引用, 如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,但是如果value是强引用,不会被清理, 这样一来就会出现 key 为 null 的 value。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal用在什么地方?
讨论ThreadLocal用在什么地方前,我们先明确下,如果仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的!!!
ThreadLocal归纳下来就2类用途:
- 保存线程上下文信息,在任意需要的地方可以获取!!!
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
保存线程上下文信息,在任意需要的地方可以获取
由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。
还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但是ThreadLocal也有局限性,我们来看看阿里规范:
每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的,所以ThreadLocal无法解决共享对象的更新问题!
set()方法
public void set(T value) {
//获取到当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null){
//将值存入map中,key就是调用set()的当前ThreadLocal对象
map.set(this, value);
}
else {
createMap(t, value);
}
}
最后存储到Entry[]数组里面
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
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();
}
简单测试
public class ThreadLocalTest2 {
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
/**
* set是set进当前线程的ThreadLocalMap,key是ThreadLocal对象
* 所以一个ThreadLocal对象连续调用set()方法时,会覆盖已经保存的值
* 如果一个线程要保存多个本地线程变量,则应该使用多个ThreadLocal对象
*/
threadLocal1.set(i);
threadLocal2.set(i+"_@");
System.out.println(Thread.currentThread().getName() + ":数字——" + threadLocal1.get());
System.out.println(Thread.currentThread().getName() + ":字符——" + threadLocal2.get());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal1.remove();
threadLocal2.remove();
}
}, "线程1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + threadLocal1.get());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal1.remove();
}
}, "线程2").start();
}
}
线程1:数字——0
线程1:字符——0_@
线程2:null
线程2:null
线程1:数字——1
线程1:字符——1_@
线程2:null
线程1:数字——2
线程1:字符——2_@
线程2:null
线程1:数字——3
线程1:字符——3_@
线程2:null
线程1:数字——4
线程1:字符——4_@
线程1:数字——5
线程1:字符——5_@
线程2:null
线程2:null
线程1:数字——6
线程1:字符——6_@
线程2:null
线程1:数字——7
线程1:字符——7_@
线程2:null
线程1:数字——8
线程1:字符——8_@
线程2:null
线程1:数字——9
线程1:字符——9_@
Thread类有属性变量threadLocals (类型是ThreadLocal.ThreadLocalMap),也就是说每个线程有一个自己的ThreadLocalMap ,所以每个线程往这个ThreadLocal中读写隔离的,并且是互相不会影响的。
一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!!!
原因:
ThreadLocal只能存放一个Object是因为它的底层实现使用的是ThreadLocalMap,ThreadLocalMap的key是ThreadLocal对象本身,而value则是用户存储的Object。由于ThreadLocalMap的key是ThreadLocal对象本身,因此每个ThreadLocal对象只能存储一个Object。
使用注意:参考文章
- 每个线程都有一个 ThreadLocalMap(ThreadLocal内部类),Map 中元素的键为ThreadLocal,而值对应线程的变量副本。Map是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。
- ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
- 一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
当仅仅只有ThreadLocalMap中的Entry的key指向ThreadLocal的时候,ThreadLocal会被进行回收,ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值会变成null,但是Entry是强引用,那么Entry里面存储的Object,并没有办法进行回收,所以ThreadLocalMap 做了一些额外的回收工作。
由于线程的生命周期很长,如果我们往ThreadLocal里面set了很大很大的Object对象,虽然set、get等等方法在特定的条件会调用进行额外的清理,但是ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值会变成null,但是后续在也没有操作set、get等方法了,应该在我们不使用的时候,主动调用remove方法进行清理。
这里把ThreadLocal定义为static还有一个好处就是,由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除