Java异步编程(二):为什么我的异步任务没被执行

为什么我的异步任务没被执行

  • 1、概述
  • 2、为什么我的异步任务没被执行
    • 2.1、真的是异步吗
    • 2.2、模拟耗时任务
    • 2.4、使用自定义线程池
    • 2.4、关闭线程池
    • 2.5、关闭线程池——更进一步
  • 3、总结

1、概述

大家好,我是欧阳方超。在上一篇文章中——,我们介绍了CompletableFuture的初步使用,只是在那里我们并没有从代码的角度验证是程序异步执行的,今天就来看一下。

2、为什么我的异步任务没被执行

2.1、真的是异步吗

将上一篇文章中的代码稍作调整:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->  "期望后输出");

        future.thenAccept(s -> {
            System.out.println(s);
        });

        System.out.println("期望首先输出");

    }
}

程序输出结果:

期望后输出
期望首先输出

这个输出结果就值得思考了,为什么不是先输出“期望首先输出”这一行文字,而是先输出了“期望后输出”呢,按理说主线程提交了一个异步任务,异步任务由其他线程执行,主线程不等待异步任务继续往下执行,好吧,其实这种情况就是上面代码中异步任务完成的速度可能比较快,因此thenAccept()方法注册的Consumer函数会在主线程输出第一个语句之前就已经被调用了,从而导致输出了上面的结果。
不想让异步任务执行那么快,那么首先想到的是使用sleep()方法模拟一个耗时任务,具体改动如下。

2.2、模拟耗时任务

在异步任务中模拟耗时任务,代码如下:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {


        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "期望后输出";
        });

        future.thenAccept(s -> {
            System.out.println(s);
        });
        System.out.println("期望首先输出");

    }
}

此时只见程序输出了:

期望首先输出

异步任务中的字符串根本没有输出,为什么呢?实际上,JVM只会等待new出来的线程执行完毕后才会退出,不会等待守护进程,而CompletableFuture底层默认使用了ForkJoinPool.commonPool()方法返回的线程,这些线程都是守护线程,所以主线程一旦结束,不会等待异步任务的完成了,进而JVM也退出了。异步任务没有执行,这显然不是我们想要的结果,有办法解决吗,当然有,那就是使用自定义线程池。

2.4、使用自定义线程池

CompletableFuture默认使用的是ForkJoinPool.commonPool()方法返回的线程,好在CompletableFuture也提供了相关方法允许使用自定义的线程池,代码如下:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "期望后输出";
        }, executorService);

        future.thenAccept(s -> {
            System.out.println(s);
        });
        System.out.println("期望首先输出");
        
    }
}

上面代码中我们使用Executors.newFixedThreadPool(1)创建了里面包含一个线程的线程池executorService ,调用supplyAsync()方法就会把异步任务提交给executorService ,该异步任务不会立即执行(只会在合适的时机执行),supplyAsync()方法会返回一个future对象,接着主线程继续往下执行System.out.println(“期望首先输出”)语句,接着异步任务完成,thenAccept()回到才运行,最终程序输出:

期望首先输出
期望后输出

但是这个时候有个问题,就是JVM会一直不退出,因为我们自己创建的线程池依然存活着呢,还在等待任务进来,也就是我们没有对其进行关闭,接下来看如何关闭线程池。

2.4、关闭线程池

其实关闭线程池可以使用shutdown()方法,代码如下:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "期望后输出";
        }, executorService);

        future.thenAccept(s -> {
            System.out.println(s);
        });
        executorService.shutdown();
        System.out.println("期望首先输出");
    }
}

上面的程序输出:

期望首先输出
期望后输出

2.5、关闭线程池——更进一步

如果我们只是使用shutdown()来关闭线程池,它会触发之前提交过来的任务按顺序执行完,并且此时只是不在接收新提交过来的任务;还应该调用boolean awaitTermination(long timeout, TimeUnit unit)方法等待线程池中的任务执行完成,该方法接收两个参数,一个是时长,一个是时长单位,该方法会一直阻塞直到下面条件中的其中一个首先发生:在shutdown()请求之后任务成功完成、超时时间已到、当前线程被打断,如果在设置的超时时间内任务执行完了该方法返回true、如果超时了就返回false,下面的代码中使用了boolean awaitTermination(long timeout, TimeUnit unit)方法:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "期望后输出";
        }, executorService);

        future.thenAccept(s -> {
            System.out.println(s);
        });
        executorService.shutdown();
        boolean b = executorService.awaitTermination(5, TimeUnit.SECONDS);
        if (b) {
            System.out.println("没问题");
        } else {
            System.out.println("有问题");
        }
        System.out.println("期望首先输出");
    }
}

输出结果:

期望后输出
没问题
期望首先输出

awaitTermination()方法等待5s,异步任务中的任务等待1s,在等待时间内异步任务完全有时间执行完,返回此时该方法返回了true,注意,异步任务执行完后future.thenAccept()方法是由主线程执行的,所有输出顺序才是上面的结果。
如果我们将awaitTermination()方法等待1s,异步任务中的任务等待5s,awaitTermination()方法将返回false,程序输出结果也会发生变了,详见下方调整后的程序:

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "期望后输出";
        }, executorService);

        future.thenAccept(s -> {
            System.out.println(s);
        });
        executorService.shutdown();
        boolean b = executorService.awaitTermination(1, TimeUnit.SECONDS);
        if (b) {
            System.out.println("没问题");
        } else {
            System.out.println("有问题");
        }
        System.out.println("期望首先输出");
    }
}

输出结果:

有问题
期望首先输出
期望后输出

3、总结

今天的介绍,我们从一个小的异步程序开始,到模拟耗时任务,再到用自定义线程池执行异步任务,最后谈到线程池的关闭,从一些方面了解了为什么有时候异步任务没有被执行。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。

你可能感兴趣的:(java,jvm,开发语言)