驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
通过一个业务BUG来理解线程池和并发的运行原理
/  

通过一个业务BUG来理解线程池和并发的运行原理

开篇

最近编写项目代码过程中,遇到了一个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 个核心的东西:核心线程、队列、临时线程 他们协同工作。

  • 任务先通过核心线程执行
  • 当核心线程满了后,会将任务放在队列
  • 当任务急剧加大,队列都满了的情况,临时线程也会加入进来
  • 当临时线程也创建满了,此时会触发异常

上述就是大概的原理了,我们来结合看看。

  1. 代码创建的是一个20个核心线程,Int.Max长度的队列的线程池
  2. 最开始我们将 20 个任务放入其中的时候,是没有问题的,能够立刻执行代码limit.incrementAndGet()
  3. 但是当 21个任务放入线程池的时候,因为核心线程可能是满的,上述的控制变量的代码不会立刻执行,而是积压在队列中,不会马上执行,此时的 count仍然可能是 20
  4. 依次类推,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又不是原子性的,所以出现了上述问题。

结语

所以并发真不是这么简单的,不过了解了原理,分析起来,应该不会太难吧。

不积跬步,无以至千里。不积小流,无以成江海。