最近编写项目代码过程中,遇到了一个BUG,这里简单记录和分享下,业务简化下,就是通过线程池执行某个任务指定的次数。
当时代码是这么写的:
@Test
fun testQueue() {
val pool = Executors.newFixedThreadPool(20)
val limit = AtomicInteger(0)
var count = 0
while (count < 50) {
pool.execute {
count = limit.incrementAndGet()
println(count)
}
}
}
在预期中,打印的次数也就是 50 次,结果最后结果远远超过了 50 次。
为什么了!?
最考试我以为 AtomictInteger
的用法用错了,但是翻看了下笔记,这个类的使用没有问题的。
当时迷糊了10 分钟,借着去上厕所后回来的时间(放松了下思路),结合所学的线程池知识,就来了感觉了。
Java 的线程池执行有 3 个核心的东西:核心线程、队列、临时线程 他们协同工作。
上述就是大概的原理了,我们来结合看看。
20
个核心线程,Int.Max
长度的队列的线程池limit.incrementAndGet()
count
仍然可能是 20
21+
的线程都有可能遇到上述 3 的情况,虽然 AtomictInteger
是并发安全的,不会出现少 加 1 的情况,但是因为加入队列的时候,没有达到临界值,所以会导致最后的执行结果远远大于 50 次的情况。很简单,加 1 的操作,不要交给线程池中的任务来做,我们在主线程进行控制即可。
@Test
fun testQueue2() {
val pool = Executors.newFixedThreadPool(20)
val limit = AtomicInteger(0)
var count = 0
while (count < 50) {
//提到外面来
count = limit.incrementAndGet()
pool.execute {
println(count)
}
}
}
这样打印出来的结果就会是50
行了,我这里打印的结果如下所示:
3
3
3
5
7
7
8
9
10
11
12
13
14
15
16
17
19
19
20
21
43
50
...
50
这个结果虽然最大是 50 行,最大的数是 50,但是为什么不是1,2,3...50
这么打印的了?
问题就出在定义的count
非线程共享的变量,多个线程都在同时的对其+1
操作,而打印
和+1
又不是原子性的,所以出现了上述问题。
所以并发真不是这么简单的,不过了解了原理,分析起来,应该不会太难吧。