静态代码块中使用 ExecutorService 执行多线程会出现什么情况呢?

AQS系列

1、AQS核心原理
2、ReentrantLock 原理及示例
3、CountDownLatch / Semaphore 示例及使用场景
4、BlockingQueue 示例及使用场景
5、静态代码块中使用 ExecutorService 执行多线程会出现什么情况呢?

一、 一般场景

我们在 MultiThreadExample.java 中 编写一个 exec 方法,这个方法中通过 Executors.newFixedThreadPool(3) 初始化一个线程池,线程数量是 3,随后则提交三个任务,最后用 executorService.shutdown() 关闭线程池,再用 awaitTermination 方法来等待所有的线程执行完,exec 方法运行结束了。
那一般我们是在 main 方法中直接调用 exec() 方法来执行,代码如下:

public class MultiThreadExample {

    public static void main(String[] args) {
        exec();
    }

    private static void exec(){
	    System.out.println("任务开始执行...");
        // 创建一个固定大小的线程池,其中包含3个线程
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 启动任务1
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务1 - 执行步骤 " + i);
            }
        });

        // 启动任务2
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务2 - 执行步骤 " + i);
            }
        });

        // 启动任务3
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务3 - 执行步骤 " + i);
            }
        });

        // 关闭线程池
        executorService.shutdown();

        try {
            // 等待所有任务执行完毕或者超时(这里设置超时为10秒)
            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                // 如果在超时时间内任务没有执行完毕,可以选择进行额外处理
                System.out.println("等待超时,可能有任务未完成");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务已经完成,程序退出。");
    }
}

运行结果:

任务开始执行...
任务1 - 执行步骤 1
任务1 - 执行步骤 2
任务1 - 执行步骤 3
任务1 - 执行步骤 4
任务1 - 执行步骤 5
任务2 - 执行步骤 1
任务2 - 执行步骤 2
任务2 - 执行步骤 3
任务2 - 执行步骤 4
任务2 - 执行步骤 5
任务3 - 执行步骤 1
任务3 - 执行步骤 2
任务3 - 执行步骤 3
任务3 - 执行步骤 4
任务3 - 执行步骤 5
所有任务已经完成,程序退出...

二、static {} 场景

其实和一般场景最大的区别就是,exec() 方法是在静态代码块中被调用的,也就是在 类加载 时就被执行的。

public class MultiThreadExample {

    static {
        exec();
    }

    public static void main(String[] args) {
	    // 这里不管是否 new 效果都一样的
        new MultiThreadExample();
    }

    private static void exec(){
    	System.out.println("任务开始执行...");
        // 创建一个固定大小的线程池,其中包含3个线程
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 启动任务1
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务1 - 执行步骤 " + i);
            }
        });

        // 启动任务2
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务2 - 执行步骤 " + i);
            }
        });

        // 启动任务3
        executorService.execute(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("任务3 - 执行步骤 " + i);
            }
        });

        // 关闭线程池
        executorService.shutdown();

        try {
            // 等待所有任务执行完毕或者超时(这里设置超时为10秒)
            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                // 如果在超时时间内任务没有执行完毕,可以选择进行额外处理
                System.out.println("等待超时,可能有任务未完成");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务已经完成,程序退出。");
    }
}

运行结果:

任务开始执行...
等待超时,可能有任务未完成
所有任务已经完成,程序退出...
任务3 - 执行步骤 1
任务2 - 执行步骤 1
任务2 - 执行步骤 2
任务2 - 执行步骤 3
任务2 - 执行步骤 4
任务2 - 执行步骤 5
任务3 - 执行步骤 2
任务3 - 执行步骤 3
任务3 - 执行步骤 4
任务3 - 执行步骤 5
任务1 - 执行步骤 1
任务1 - 执行步骤 2
任务1 - 执行步骤 3
任务1 - 执行步骤 4
任务1 - 执行步骤 5

三、原理(why?)

我们可以看到 一般场景static {} 场景 的运行结果完全不同,static {} 场景 则出现了等待超时的情况,而不是等待所有线程都运行结束,这是为什么呢?

我们知道 static{} 是一个静态代码块,当 JVM 加载 MultiThreadExample 类时,会自动调用静态块中的 exec() 方法,这意味着线程池和线程会在 main() 方法之前就开始执行了,那为什么在 static{} 中调用 exec() 方法时会出现等待超时的情况呢?

出现 等待超时,可能是有任务未完成的情况是因为主线程在调用 executorService.shutdown() 后立即尝试通过 executorService.awaitTermination(10, TimeUnit.SECONDS) 来等待所有任务完成,但此时由于线程池中的线程尚未完全启动或正在执行中,所以主线程可能在给定的超时时间内无法确认所有任务是否已完成,从而导致超时。

那为什么在 main() 方法中调用 exec() 方法就不超时,所有线程都按预期执行完成,在 static{} 静态代码块中执行就会超时呢 ?
我们在 execute(() ->{})提交了任务之后,使用了 shutdown() 方法来关闭线程池,然后立即通过 awaitTermination(10, TimeUnit.SECONDS) 来等待所有线程都完成,但是此时线程池中的线程并没有开始执行或者正在执行中,而我们到 awaitTermination 方法中打断点会看到,程序执行到了 nanos = termination.awaitNanos(nanos); 这句,如图:
等待
为啥执行到了这句,因为上面的 runStateAtLeast(ctl.get(), TERMINATED) 判断返回了 false,runStateAtLeast 方法源码如下:

private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}

说明线程池中的线程并没有执行完,c 是线程池中的目前的线程数量,s 是线程池中总的线程数量,看调用处传参就知道,传入的参数是 ctl.get()TERMINATED
termination.awaitNanos(nanos) 等待完之后,又赋值给 nanos,此时 nanos 的值是一个负数了,因为此方法中,经过了 LockSupport.parkNanos(this, nanosTimeout); 等待之后,返回是这样返回的 return deadline - System.nanoTime()

所以在 for 无线循环中,等待完 nanos 时间后,下次循环时 nanos 时小于 0 的,则返回了 false

那为什么线程池中的线程没有执行完呢?还需深究,此时先知道大概原因,后面待更新!


码友们中的大佬也可以解释下更底层原因,欢迎评论区留言讨论。