线程池中运行的线程,当等待队列未满的情况下,一定不大于核心线程数吗

通过《线程池内运行的线程抛异常,线程池会怎么办》了解到当线程执行的任务出现异常时,会将当前线程移出线程池,并新增一个线程到线程池中,我们先来回顾一下线程池的运行原理:

tpe-process

从原理图中可以看到只有当队列满了之后,才会去创建新的线程执行新加入的任务,那么到底有没有可能出现队列未满, 但是运行中的线程个数大于核心线程数?

理论上应该是不可能大于核心线程数,那么有没有意外呢?答案暂时不揭晓,我们先往下看,写几个demo测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> System.out.println(t1.hashCode() + ", " + t1.getName() + " " + "发生了异常"));
return t;
});
for (int i = 0; i < 30; i++) {
int finalI = i;
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().hashCode() + ": " + pool.getQueue().size() + ", " + (10 / finalI));
});
pool.execute(t);
}
}

运行结果

​ 第一列为线程hashcode,第二列为阻塞队列中的任务个数,第三列为计算数字

image-20200528132623899

从控制台的输出能看到一共出现了4个不同的hashcode,也就表示创建过4个线程,然后线程1869318657执行任务时出现了异常,也就是除0异常,然后将其从线程池中移出,后续不再进行任务处理。从之前的文章中我们知道将异常的线程移除之后会重新创建一个线程加入到线程池中,那么正常情况下,线程池移除一个再加入一个,数量应该不变,但是从下面的输出看到这时同时有3个线程在处理任务,但是我们的核心线程数是2,为什么会出现3?

我们再来看下任务发生异常时的后续处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 从runWorker方法中传过来的是true,所以这句目前版本中必定会被执行到
// 作用是将当前线程池中的有效线程数-1,意思也就是出现异常的线程会被从线程池中拿掉
// 为什么说是出现异常的线程会被拿掉呢?因为在try内部是一个while循环,除非关闭核心线程或运行中线程出现异常,否则不会执行到这里
if (completedAbruptly)
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 更新完成的任务数,只要是被线程池线程执行过的,不管是否出现异常,都被认为是执行成功的任务
completedTaskCount += w.completedTasks;
// 将当前Worker线程从线程池中移除销毁
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

// 一系列判断,主要是判断是否符合给线程池创建新的线程
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
// 如果核心线程允许超时回收,则不去创建线程,因为有新任务来的时候会自动创建
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果核心线程可以被回收,但是当前阻塞队列中不为空时,创建1个线程去执行任务
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 校验当前运行的线程是否大于允许运行的线程数,比如核心线程数
if (workerCountOf(c) >= min)
return;
}
// ①...
// 给线程池创建新的线程
addWorker(null, false);
}
}

我们看到在创建新线程时,会进行一系列判断,若判断全部通过,就会去给线程池的池子中创建一个新的工作线程,这里传入的core参数是false,也就是当成非核心线程来创建,难道问题出在这里?如果我此时运行的线程数小于等于核心线程数,那么再加进来一个非核心线程?我们来看看addWorker()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private boolean addWorker(Runnable firstTask, boolean core) {
// goto语法
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
// 判断当前运行的线程个数是否超过了最大线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

// ...创建线程...
return workerStarted;
}

从方法addWorker()的源码中我们看到,当我们参数core传一个false进来时,会去校验当前工作中的线程个数是否超过了最大线程数,如果没有超过的话则创建新的线程,问题就出在这里,为什么?

在方法processWorkerExit()中,我们对核心线程数进行的比较,但是如果在①处有新的任务调用execute()进来,则发现工作线程未达到核心线程数,这个时候就会去创建一个核心线程,创建之后,线程池中的工作线程就有2个了,然后我们从processWorkerExit()中传过来的请求会去校验工作线程个数是否超过了最大线程数,在我们的demo代码中是肯定不会超过的,那么就又会去创建一个线程放到线程池中,这样线程池中的工作线程就有3个了,具体流程图如下

正常流程 异常流程 工作线程数
execute() 0
—addWorker() 1
execute() 1
—addWorker() 2
processWorkerExit() 2
—remove() 1
execute() 1
—addWorker() 2
—addWorker() 3

这就是为什么我们核心线程数是2,但是最终运行中的线程个数是3的原因


🌰场景

  1. 任务尚未全部进入到队列中,队列未满

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) {
    ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((t1, e) -> System.out.println(t1.hashCode() + ", " + t1.getName() + " " + "发生了异常"));
    return t;
    });
    for (int i = 0; i < 30; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
    System.out.println(Thread.currentThread().hashCode() + ": " + pool.getQueue().size() + ", " + (10 / finalI));
    });
    pool.execute(t);
    }
    }
    image-20200528132623899

    这个也就是我们上面跑的demo,工作线程个数超出了核心线程数。

  2. 任务已经全部进入到队列中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void main(String[] args) {
    ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((t1, e) -> System.out.println(t1.hashCode() + ", " + t1.getName() + " " + "发生了异常"));
    return t;
    });
    for (int i = 0; i < 30; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
    if (finalI < 2) {
    try {
    // 前两个线程等待500毫秒,保证所有的任务都进入到队列中
    Thread.sleep(500);
    } catch (InterruptedException e) {
    }
    }
    System.out.println(
    Thread.currentThread().hashCode() + ": " + pool.getQueue().size() + ", " + (10 / (finalI - 4)));
    });
    pool.execute(t);
    }
    }
    image-20200528170037838

    我们看到当线程池开始执行任务时,已经把剩余的28个任务都放到队列中了,红色和蓝色下划线的线程是初始线程,当873出现异常后,将其移出线程池,然后创建一个新的线程执行队列中的剩余任务,因为这时所有的任务都在队列中等待被执行,execute()方法不会被调用,所以这种场景下,工作中的线程个数不会超过核心线程数。

  3. 任务尚未全部进入到队列中,队列已满,未达到最大线程数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void main(String[] args) {
    ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((t1, e) -> System.out.println(t1.hashCode() + ", " + t1.getName() + " " + "发生了异常"));
    return t;
    });
    for (int i = 0; i < 34; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
    if (finalI < 2) {
    try {
    // 等待任务全部进入到队列中,且创建完额外线程接收多出来的两个任务
    Thread.sleep(500);
    } catch (InterruptedException e) {
    }
    }
    System.out.println(Thread.currentThread().hashCode() + ": " + pool.getQueue().size() + ", "
    + pool.getActiveCount() + ", " + (10 / (finalI - 33)));
    });
    pool.execute(t);
    }
    }

    运行结果

    ​ 第一列为线程hashcode,第二列为阻塞队列中的任务个数,第三列为当前的工作线程个数,第四列为计算数字

    image-20200528173106691

    从结果中我们看到创建了4个工作线程去执行任务,当线程230执行异常之后,创建了线程734继续执行,线程个数未超过最大线程数

  4. 任务尚未全部进入到队列中,队列已满,达到最大线程数

    这种情况就会依据拒绝策略执行相关的代码逻辑,线程数不会超过最大线程数


总结

从以上分析我们可以得出,只有在:任务尚未全部进入到队列中且队列未满的情况下,才会出现工作线程个数大于核心线程数,所以我们在使用线程池的过程中,待执行的任务尽量捕获所有的异常情况,不要将其抛出到线程池中的线程里。