java8 异步 并发_Java8 新特性并发篇(一) | 线程与执行器

本文详细介绍了Java8中并发编程的基础,包括线程与Runnable、ExecutorService的使用、Callable与Future的概念,以及如何设置超时、批量执行和调度任务。文章通过实例代码展示了如何利用lambda表达式简化并发编程,同时讲解了ScheduledExecutorService的使用,帮助读者掌握Java8的并发处理技巧。
摘要由CSDN通过智能技术生成

e73dc2d7c64e1290ff2b5f4f119c650a.png本文翻译整理自 https://winterbe.com/posts/2015/04/07/java8-concurrency-tutorial-thread-executor-examples/

欢迎进入 Java8 并发篇系列。此系列大概包括三篇教程:线程与执行器 Threads 与 Executors

同步与锁 Synchronization and Locks

原子变量与 ConcurrentMap

本篇博文是此系列的第一篇,接下来的 15 分钟里,我将会通过一些简单易懂的示例代码来教会你,如何在 Java8 中进行并发编程,学会如何通过 Thread, Runable 和 Executor 来并行执行代码。

关于 JDK 中 并发 API 是在 JDK1.5 中被首次引入的,并且在后续的版本中得到不断地增强。这篇文章中介绍的大部分名词概念同样适用于老版本,这一点你不用担心。本文的着重点在代码演示,如何使用 lambda 表达式以及新特性相关。

如果你对 lambda 表达式不是很熟悉,我推荐你先阅读我之前的文章:干货 | Java8 新特性指导手册

目录一、Threads 与 Runnables

二、执行器 Executor

三、Callable 和 Future

四、超时设置

五、invokeAll

六、invokeAny

七、任务调度 ScheduledExecutor

054be6c4fa7f795325675a6148d213bd.png

Threads 与 Runnables

可以肯定的是,所有的现代操作系统支持并发的手段无外乎进程和线程。进程通常可以理解为独立运行的程序实例,打个比方,你启动一个 Java 程序,这个时候操作系统就会创建一个新的进程,它可以与其他程序并行运行。而在这些进程的内部,我们可以通过线程来并发执行一段代码,一段业务逻辑。这样做有啥好处?好处就是,我们可以最大程度的发挥机器多核 CPU 的优势。

Java 从 1.0 版本就开始支持线程了。可以说是最基本的功能了,在老版本中,我们通常会实现 Runnable 接口,重写 run() 方法, 在方法内编写一段业务代码:Runnabletask=()->{

StringthreadName=Thread.currentThread().getName();

System.out.println("Hello "+threadName);

};

task.run();

Threadthread=newThread(task);

thread.start();

System.out.println("Done!");

因为 Runnable 接口是一个函数式接口,我们直接采用 lambda 表达式来书写它,内部的业务逻辑是打印当前的线程名,控制台的输出可能存在两种情况:Hello mainHello Thread-0Done!

或者:Hello mainDone!Hello Thread-0

我们无法预测 runnable 是在主线程执行完成后执行还是之前执行,因为这种顺序的不确定性,使得在一个体量庞大的应用中,并发编程变得异常复杂。

我们可以在线程内部设置休眠时间,来模拟一个长时间运行的任务:Runnablerunnable=()->{

try{

Stringname=Thread.currentThread().getName();

System.out.println("Foo "+name);

// 休眠一秒

TimeUnit.SECONDS.sleep(1);

System.out.println("Bar "+name);

}

catch(InterruptedExceptione){

e.printStackTrace();

}

};

Threadthread=newThread(runnable);

thread.start();

运行上面的代码,你会发现两条打印语句之间会间隔一秒。 TimeUnit 在处理单位时间的时候,是一个很实用的枚举类,你也可以通过调用 Thread.sleep(1000) 来达到同样的目的。

我们不得不承认,使用 Thread 类是很乏味的且容易出错的。由于 Java 并发相关的 API 是在 2004 年 Java5 发布的时候才被正式引入的。它们被放置在 java.util.concurrent 包下,里面包含了很多处理并发编程有用的类。

这些并发 API 被引入后,在后续的 Java 版本中,又得到不断的增强,Java8 甚至提供了新的并发类和方法来处理并发。

废话不多说,接下来,让我们进入并发 API 中最重要的执行器: Executor.

执行器 Executor

设计 ExecutorService 的目的是用来替代我们直接手动创建 Thread。 Executors 支持运行异步任务,通常管理着一个线程池,线程池内部线程会得到复用,避免了频繁创建线程,销毁线程而带来的额外的系统开销。// 创建一个只包含一个线程的线程池

ExecutorServiceexecutor=Executors.newSingleThreadExecutor();

executor.submit(()->{

StringthreadName=Thread.currentThread().getName();

System.out.println("Hello "+threadName);

});

// => Hello pool-1-thread-1

Executors 类提供了便利的工厂方法用来不同类型的线程池。上面的示例中我们创建了一个只包含单线程的线程池 executor。

代码的输出结果与上面手动 new Runnable() 输出一致,唯一不同的是,Java 进程没有终止!

f9cb4d505d6a102b310956f01d30261d.png注意: Executors 必须显示的终止它,否则它们将持续监听是否有新的任务需要执行。这也是为什么我在研发组中推荐小组人员创建线程池务必要交给 Spring Ioc 容器管理的原因。

那么,如何优雅的关闭 ExecutorService 呢?

ExecutorService 提供了两个方法:1. shutdwon() : 它会等待正在执行的任务执行完成后,再销毁线程池;

2. shutdownNow(): 它会立刻终止正在执行的任务,并销毁线程池;

关闭 ExecutorService 最佳实践:try {    System.out.println("attempt to shutdown executor");    executor.shutdown();    // 指定关闭之前的等待时间 5s,达到“温柔滴”关闭    executor.awaitTermination(5, TimeUnit.SECONDS);}catch (InterruptedException e) {    System.err.println("tasks interrupted");}finally {    if (!executor.isTerminated()) {        System.err.println("cancel non-finished tasks");    }    executor.shutdownNow();    System.out.println("shutdown finished");}

Callable 和 Future

除了 Runnable, executor 还支持另外一种任务类型 —— Callable。 Callable 和 Runnable类似,唯一不同的是, Callable 有返回值。

下面的示例代码中,我们通过 lambda 表达式定义了一个 Callable:休眠 1s 钟后,返回一个整数。Callable task = () -> {    try {        TimeUnit.SECONDS.sleep(1);        return 123;    }    catch (InterruptedException e) {        throw new IllegalStateException("task interrupted", e);    }};

Callable 也可以像 Runnable 一样,作为入参,提交给 executor。这样的话,问题来了,我们怎么取到返回值呢?因为 submit() 方法不会阻塞等待任务完成的。

虽然 executor 不能直接返回 Callable 结果,不过, executor 可以返回一个 Future 类型的结果,他可以在稍后的某个时机取出实际的返回值。ExecutorServiceexecutor=Executors.newFixedThreadPool(1);

Futurefuture=executor.submit(task);

System.out.println("future done? "+future.isDone());

// 这里会阻塞等待返回结果

Integerresult=future.get();

System.out.println("future done? "+future.isDone());

System.out.print("result: "+result);

上面的这段代码, Callable 提交给 executor 后,我们先通过调用 isDone() 检查 future 是否执行完成,结果当然是 false, 因为 future 在返回那个整数之前,会休眠 1s。

后面到执行 get() 方法,线程会阻塞等待返回结果。整个流程走完,我们再来看下控制台的输出:future done? falsefuture done? trueresult: 123

你需要注意,如果你关闭 executor, 所有为关闭的 future 都会抛出异常。executor.shutdownNow();future.get();

还有一点,你可能也注意到了,这次我们创建线程池并非使用 newSingleThreadExecutor()。而是通过 newFixedThreadPool(1)来创建一个单线程线程池的 executor。 它等同于使用 newSingleThreadExecutor,不过使用第二种方式我们动态的调整入参,比如传入一个比 1 大的值来增加线程池的大小。

超时 Timeouts

上面提到了, future.get() 那里会阻塞等待,直到 callable 返回值。我们试想一个最糟糕的情况, callable 持续运行导致你的程序没有响应,这显然是不能忍受的。我们可以通过传入一个超时时间来避免发生这种情况:ExecutorServiceexecutor=Executors.newFixedThreadPool(1);

Futurefuture=executor.submit(()->{

try{

TimeUnit.SECONDS.sleep(2);

return123;

}

catch(InterruptedExceptione){

thrownewIllegalStateException("task interrupted",e);

}

});

// 设置超时时间为 1s

future.get(1,TimeUnit.SECONDS);

执行上面这段代码,会抛出一个 TimeoutException:Exception in thread "main" java.util.concurrent.TimeoutException    at java.util.concurrent.FutureTask.get(FutureTask.java:205)

为什么抛出这个异常,原因很显然,我们指定的超时时间是 1s, 而任务中光休眠就是 2s 了。

invokeAll

Executors 支持通过 invokeAll 方法一次性批量提交多个callable。这个方法的返回结果是一个 future 集合:ExecutorServiceexecutor=Executors.newWorkStealingPool();

List>callables=Arrays.asList(

()->"task1",

()->"task2",

()->"task3");

executor.invokeAll(callables)

.stream()

.map(future->{

try{

// 拿到每个 future 的返回值

returnfuture.get();

}

catch(Exceptione){

thrownewIllegalStateException(e);

}

})

.forEach(System.out::println);// for 循环输出结果

上面这段示例代码,我们先利用 Java8 的 Stream 流来处理 invokeAll() 方法返回的 future集合,取出每个 future 的返回值将其映射到一个 List 集合中,最后循环输出结果。

invokeAny

批量提交的另外一种方式是 invokeAny() , 同样是批量提交,它与 invokeAll() 不同点在于:它会阻塞等待,当某个 callable 第一个执行完成,它会立刻返回执行结果,而不再等待那些正在执行中的 callable 了。

为了测试,我们定义一个方法,用来模拟创建拥有不同执行时间的 callable:Callable callable(String result, long sleepSeconds) {    return () -> {        TimeUnit.SECONDS.sleep(sleepSeconds);        return result;    };}

然后,我们创建一组 callable, 他们拥有不同的执行时间,1s 到 3s 的,通过 invokeAny()将这些 callable 提交给 executor,看看返回最快的 callable 的字符串结果是哪个:ExecutorServiceexecutor=Executors.newWorkStealingPool();

List>callables=Arrays.asList(

callable("task1",2),

callable("task2",1),

callable("task3",3));

Stringresult=executor.invokeAny(callables);

System.out.println(result);

// => task2

最快的是 task2, 它的执行时间最短,所以输出是它。

上面这段示例代码中,创建 ExecutorService 的方式又变了,通过 Executors.newWorkStealingPool()。这个工厂方法是 Java8 才引入的,返回值是一个 ForkJoinPool类型的 executor,它和指定固定大小的线程池不同, ForkJoinPools 允许指定一个并行因子来创建,默认的值为物理机可用的 CPU 核心数。

ForkJoinPools 是在 Java7 中才被引入的,这将会在后面系列的教程中做详细介绍。敬请期待。

任务调度 ScheduledExecutor

上面我们已经学习了如何在一个 executor 中提交和运行一次任务。还有种场景是,我们需要多次运行一个常见的任务,这个时候,我们可以利用调度线程池。

ScheduledExecutorService支持任务调度,持续执行或者延迟一段时间后再执行。

下面的代码示例,指定一个任务延迟 3 分钟再执行:ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(1);

Runnabletask=()->System.out.println("Scheduling: "+System.nanoTime());

ScheduledFuture>future=executor.schedule(task,3,TimeUnit.SECONDS);

TimeUnit.MILLISECONDS.sleep(1337);

longremainingDelay=future.getDelay(TimeUnit.MILLISECONDS);

System.out.printf("Remaining Delay: %sms",remainingDelay);

ScheduledExecutorService 会返回一个增强类型的 future 类型 —— ScheduleFuture,它除了保有原有 Future 提供的所有方法外,还提供了 getDelay() 方法, 用来获取距离目标时间的时间差,也就是剩余的延迟时间。

为了保证调度任务的持续执行, executors 提供了两个 API:1. scheduleAtFixedRate() :固定周期执行一个任务, 不考虑任务的耗时,即使上个任务还没有执行完毕,到了时间,下个任务依然会去执行;ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(1);

Runnabletask=()->System.out.println("Scheduling: "+System.nanoTime());

// 第一次执行的延迟时间

intinitialDelay=0;

// 周期频率

intperiod=1;

// 指定任务立刻执行,且每次之间间隔为 1s

executor.scheduleAtFixedRate(task,initialDelay,period,TimeUnit.SECONDS);注意: initialDelay 表示第一次执行的延迟时间,0 表示立即执行2. scheduleWithFixedDelay() :固定延迟执行任务,就是说,必须要等到上个任务执行完成后才开始计时,达到设定的延迟时间后,下个任务才会被触发;ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(1);

Runnabletask=()->{

try{

TimeUnit.SECONDS.sleep(2);

System.out.println("Scheduling: "+System.nanoTime());

}

catch(InterruptedExceptione){

System.err.println("task interrupted");

}

};

executor.scheduleWithFixedDelay(task,0,1,TimeUnit.SECONDS);总结一下: scheduleWithFixedDelay() 和 scheduleAtFixedRate() 最大的区别就是, scheduleWithFixedDelay()需要等到前一个任务执行完成后才开始计延时,再触发下一个任务。

如上面的示例代码中这个调度任务,设置了 1s 的延时,初始化延时为 0,假设理想情况下任务的耗时就是 2s, 则任务的调度周期为: 0s->3s->6s->9s...., 它更适用于当你无法预测任务的执行时长的场景中使用。

总结

这篇文章主要探讨了如何在用 lambda 表达式,在 Java8 中使用线程和执行器,并演示了相关示例代码。除此之外,我们还学习了 Callable 和 Future, 超时设置,任务的批量提交 invokeAll() 和 invokeAny() API。最后, 了解了如何在 Java8 中使用任务调度 ScheduledExecutor,希望你能有所收获。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值