背景
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,说明该私有变量是所有线程共享的。在大量请求并发时可能就会出现问题。
在我这个项目里,最大线程数是 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