Java坑之线程安全——并发容器之抛砖引玉(ThreadLocal)

1.没有意识到线程重用导致用户信息错乱的 Bug

ThreadLocal:

首先我们先了解一下这个类:

threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

解释一下,就是说:

/**
 *大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应 
 *的互相独立的。通过get和set方法就可以得到当前线程对应的值。
*/

通过上面的解释,我们可以猜想到它的对应工作场景:

适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal 中缓存数据是比较合适的做法

但在获取的时候我们偶尔会出现一个情况就是---在Spring Boot的框架下,在Tomcat里,有时获取到的用户信息是别人的。

这个问题的解答其实很好理解:

那么我们可以先了解到:Tomcat的工作线程是基于线程池的

这样的话我么们就可以推论出:“线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。”--------极客时间《Java 业务开发常见错误 100 例》

下面给一个测试用例:


private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);


@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
    //设置用户信息之前先查询一次ThreadLocal中的用户信息
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    //设置用户信息到ThreadLocal
    currentUser.set(userId);
    //设置用户信息之后再查询一次ThreadLocal中的用户信息
    String after  = Thread.currentThread().getName() + ":" + currentUser.get();
    //汇总输出两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

然后,我们把Tomcat的参数也修改一下(配置文件中进行),将线程数设置为1,这样能更快重现一下场景


server.tomcat.max-threads=1

然后分别用两个用户去请求,第一个会显然是没有问题的,而第二个就会获取到第一个用户的信息,这个就是Tomcat 的线程池重用了线程所导致的。

而如何去解决这个问题呢,我们只需要显性的去清理一下数据,具体代码修改如下,你可以进行对比验证:


@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

从这一个我们可以了解到两点:

1.不能认为没有显式开启多线程就不会有线程安全问题

2.使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。

同样的,ThreadLocal 是利用独占资源的方式,来解决线程安全问题,那如果我们确实需要有资源在线程之间共享,我们可能就需要用到线程安全的容器,那就是下面的话题了

使用了线程安全的并发工具,并不代表解决了所有线程安全问题

这个就交到下一篇来更新吧!!

具体请关注微信公众号月夜Moonlight,文章链接:https://mp.weixin.qq.com/s/Db1HYvhXOnjvWMC2FkPeSw

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值