ThreadLocal,TransmittableThreadLocal


参考文献:
手撕面试题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_@
线程2null
线程2null
线程1:数字——1
线程1:字符——1_@
线程2null
线程1:数字——2
线程1:字符——2_@
线程2null
线程1:数字——3
线程1:字符——3_@
线程2null
线程1:数字——4
线程1:字符——4_@
线程1:数字——5
线程1:字符——5_@
线程2null
线程2null
线程1:数字——6
线程1:字符——6_@
线程2null
线程1:数字——7
线程1:字符——7_@
线程2null
线程1:数字——8
线程1:字符——8_@
线程2null
线程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的时候就可以正确进行定位到并且删除

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值