Spring MVC 中的线程安全

本文通过实验展示了在Spring MVC中,@Component注解的私有变量不是线程安全的,可能导致并发问题。使用ThreadLocal可以实现线程隔离,但每个线程拥有独立的计数器。而AtomicInteger则能保证在多线程环境下的原子性操作,确保线程安全。实验揭示了在高并发场景下,如何选择合适的数据结构来处理共享变量。
摘要由CSDN通过智能技术生成

背景

Spring MVC 这个框架我前前后后用了也有快半年了,但还真的从来没有遇到过线程安全相关的问题。

直到这周做了这样一个需求:

  • 我们的平台出售充值码,需要定期检查每款产品的充值码剩余库存,库存过低则报警 / 下架;
  • 查询剩余库存需要遍历记录,比较耗费性能,不能每次发奖都查询;
  • 常规的方案是,每隔固定时间轮询一次,比如每分钟查询一次,但我觉得不如每发固定个充值码查询一次好,比如单台机器每发 10 个充值码查询一次。
  • 如果这样的话,就需要每台机器维护一个 线程安全 的字典,记录每种充值码发放了几次。

(如果我们维护了一个“剩余库存”的数字就好了,可惜没有)

写到这儿我又想,其实每次查询剩余库存也不需要遍历啊!只要查出来的数量够多,就用再遍历“到底有多少个”了。可以按这个思路再改一版,不过这就是后话了。

实验

实验 1:@Component 中的私有变量默认不是线程安全的

实验代码如下,在 controller 中定义了一个私有变量 counter,处理请求时分别打印线程 ID、counter 值:

@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {
    private Integer counter = 0; // 私有变量 counter

    @GetMapping("/test")
    public String test() {
        counter ++; // counter ++

		// 分别打印线程 ID、counter 值
        log.info("thread: {}, counter value: {}", Thread.currentThread().getId(), counter);

        return "asdfpoiu";
    }
}

连续请求上面的 /test/test 接口 13 次:

2021-07-23 18:23:24.641 - INFO 17992 --- [nio-8157-exec-2] **.controllers.TestController thread: 55, counter value: 1
2021-07-23 18:23:27.359 - INFO 17992 --- [nio-8157-exec-3] **.controllers.TestController thread: 56, counter value: 2
2021-07-23 18:23:30.085 - INFO 17992 --- [nio-8157-exec-4] **.controllers.TestController thread: 57, counter value: 3
2021-07-23 18:23:31.787 - INFO 17992 --- [nio-8157-exec-5] **.controllers.TestController thread: 58, counter value: 4
2021-07-23 18:23:33.563 - INFO 17992 --- [nio-8157-exec-6] **.controllers.TestController thread: 59, counter value: 5
2021-07-23 18:23:35.721 - INFO 17992 --- [nio-8157-exec-7] **.controllers.TestController thread: 60, counter value: 6
2021-07-23 18:23:38.342 - INFO 17992 --- [nio-8157-exec-8] **.controllers.TestController thread: 61, counter value: 7
2021-07-23 18:23:40.799 - INFO 17992 --- [nio-8157-exec-9] **.controllers.TestController thread: 62, counter value: 8
// 下面这行很有意思,exec-10 多占了一位,导致 nio 变成了 io
2021-07-23 18:23:45.816 - INFO 17992 --- [io-8157-exec-10] **.controllers.TestController thread: 63, counter value: 9
2021-07-23 18:23:47.434 - INFO 17992 --- [nio-8157-exec-1] **.controllers.TestController thread: 54, counter value: 10
2021-07-23 18:23:51.058 - INFO 17992 --- [nio-8157-exec-2] **.controllers.TestController thread: 55, counter value: 11
2021-07-23 18:24:37.291 - INFO 17992 --- [nio-8157-exec-5] **.controllers.TestController thread: 58, counter value: 12
2021-07-23 18:24:39.401 - INFO 17992 --- [nio-8157-exec-6] **.controllers.TestController thread: 59, counter value: 13

很有意思,发现在处理前 10 个请求的时候,Spring 用了 10 个不同的线程(ID 54 ~ 63),直到从第 11 个请求开始才重复利用之前的线程。

而且可以看到,counter 的值从 0 递增到了 13,说明该私有变量是所有线程共享的。在大量请求并发时可能就会出现问题。

链接:Spring Boot 查看线程池大小

在我这个项目里,最大线程数是 1000。不知道为什么上面只出现了 10 个线程,可能是别的地方的设置吧:

server:
  tomcat:
    basedir:
    max-threads: 1000

实验 2:ThreadLocal

使用 ThreadLocal 可以解决线程不安全问题,但这就意味着线程之间不再共享 counter 了:

@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {
    private ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0); // 线程单独变量

    @GetMapping("/test")
    public String test() {
        counter.set(counter.get() + 1);

        log.info("thread: {}, counter value: {}", Thread.currentThread().getId(), counter.get());

        return "asdfpoiu";
    }
}

同样请求 13 次,发现计数器的值变成线程独有的了:

2021-07-23 19:27:07.674 - INFO 15424 --- [nio-8157-exec-2] **.controllers.TestController thread: 52, counter value: 1
2021-07-23 19:27:11.308 - INFO 15424 --- [nio-8157-exec-3] **.controllers.TestController thread: 53, counter value: 1
2021-07-23 19:27:12.618 - INFO 15424 --- [nio-8157-exec-4] **.controllers.TestController thread: 54, counter value: 1
2021-07-23 19:27:13.861 - INFO 15424 --- [nio-8157-exec-5] **.controllers.TestController thread: 55, counter value: 1
2021-07-23 19:27:14.970 - INFO 15424 --- [nio-8157-exec-6] **.controllers.TestController thread: 56, counter value: 1
2021-07-23 19:27:17.454 - INFO 15424 --- [nio-8157-exec-7] **.controllers.TestController thread: 57, counter value: 1
2021-07-23 19:27:19.245 - INFO 15424 --- [nio-8157-exec-8] **.controllers.TestController thread: 58, counter value: 1
2021-07-23 19:27:20.130 - INFO 15424 --- [nio-8157-exec-9] **.controllers.TestController thread: 59, counter value: 1
2021-07-23 19:27:20.876 - INFO 15424 --- [io-8157-exec-10] **.controllers.TestController thread: 60, counter value: 1
2021-07-23 19:27:21.596 - INFO 15424 --- [nio-8157-exec-1] **.controllers.TestController thread: 51, counter value: 1
2021-07-23 19:27:22.337 - INFO 15424 --- [nio-8157-exec-2] **.controllers.TestController thread: 52, counter value: 2
2021-07-23 19:27:23.960 - INFO 15424 --- [nio-8157-exec-3] **.controllers.TestController thread: 53, counter value: 2
2021-07-23 19:27:24.573 - INFO 15424 --- [nio-8157-exec-4] **.controllers.TestController thread: 54, counter value: 2

实验 3:AtomicInteger

AtomicInteger 也能解决线程不安全的问题,可以保证每个线程更新 counter 值是依次进行的:

@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {
    private AtomicInteger counter = new AtomicInteger(0); // Atomic Integer

    @GetMapping("/test")
    public String test() {

        log.info("thread: {}, counter value: {}", Thread.currentThread().getId(), counter.incrementAndGet());

        return "asdfpoiu";
    }
}

这里就不模拟大量并发请求了,还是顺序请求 13 次,这么看的话效果其实和实验 1 中一样:

2021-07-23 19:41:25.207 - INFO 23992 --- [nio-8157-exec-1] **.controllers.TestController thread: 52, counter value: 1
2021-07-23 19:41:26.405 - INFO 23992 --- [nio-8157-exec-2] **.controllers.TestController thread: 53, counter value: 2
2021-07-23 19:41:27.507 - INFO 23992 --- [nio-8157-exec-4] **.controllers.TestController thread: 55, counter value: 3
2021-07-23 19:41:28.315 - INFO 23992 --- [nio-8157-exec-6] **.controllers.TestController thread: 57, counter value: 4
2021-07-23 19:41:29.195 - INFO 23992 --- [nio-8157-exec-5] **.controllers.TestController thread: 56, counter value: 5
2021-07-23 19:41:29.943 - INFO 23992 --- [nio-8157-exec-7] **.controllers.TestController thread: 58, counter value: 6
2021-07-23 19:41:30.710 - INFO 23992 --- [nio-8157-exec-8] **.controllers.TestController thread: 59, counter value: 7
2021-07-23 19:41:31.342 - INFO 23992 --- [nio-8157-exec-9] **.controllers.TestController thread: 60, counter value: 8
2021-07-23 19:41:32.235 - INFO 23992 --- [io-8157-exec-10] **.controllers.TestController thread: 61, counter value: 9
2021-07-23 19:41:33.034 - INFO 23992 --- [nio-8157-exec-3] **.controllers.TestController thread: 54, counter value: 10
2021-07-23 19:41:33.871 - INFO 23992 --- [nio-8157-exec-1] **.controllers.TestController thread: 52, counter value: 11
2021-07-23 19:41:34.621 - INFO 23992 --- [nio-8157-exec-2] **.controllers.TestController thread: 53, counter value: 12
2021-07-23 19:41:35.512 - INFO 23992 --- [nio-8157-exec-4] **.controllers.TestController thread: 55, counter value: 13
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值