并发API首先随Java 5的发布引入,然后逐渐增强每个新的Java版本。 本文中显示的大多数概念也适用于较旧版本的Java。 但是我的代码示例集中在Java 8,并大量使用lambda表达式和其他新功能。 如果你还不熟悉lambdas,我建议先阅读我的Java 8教程。
Threads and Runnables
所有现代操作系统都通过进程和线程支持并发。 过程是通常彼此独立地运行的程序的实例,例如, 如果你启动一个java程序,操作系统产生一个与其他程序并行运行的新进程。 在这些进程中,我们可以利用线程同时执行代码,因此我们可以充分利用CPU的可用内核。
Java支持自JDK 1.0以来的Threads。 在启动新线程之前,必须指定要由此线程执行的代码,通常称为任务。 这是通过实现Runnable - 一个定义单个void no-args方法run()的功能接口来完成的,如下例所示:
Runnable task = () -> {
String threadName = Thread.currentThread().getName();
System.out.println("Hello " + threadName);
};
task.run();
Thread thread = new Thread(task);
thread.start();
System.out.println("Done!");
由于Runnable是一个功能接口,我们可以使用Java 8 lambda表达式将当前线程名称打印到控制台。 首先,我们在启动新线程之前在主线程上直接执行runnable。
结果如下:
Hello main
Hello Thread-0
Done!
or
Hello main
Done!
Hello Thread-0
由于并发执行,我们无法预测runnable将在打印“done”之前或之后被调用。 顺序是非确定性的,因此使并发编程在更大的应用程序中是一个复杂的任务。
线程可以进入睡眠一段时间。 这是很方便的在本文的后续代码示例中模拟长时间运行的任务:
Runnable runnable = () -> {
try {
String name = Thread.currentThread().getName();
System.out.println("Foo " + name);
TimeUnit.SECONDS.sleep(1);
System.out.println("Bar " + name);
}
catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
当你运行上面的代码,你会注意到第一个和第二个打印语句之间的一秒钟的延迟。 TimeUnit是使用时间单位的有用枚举。 或者,您可以通过调用Thread.sleep(1000)实现相同。
使用Thread类可能非常乏味和容易出错。 由于这个原因,并发API在2004年推出,随着Java 5的发布。该API位于java.util.concurrent包中,并且包含许多用于处理并发编程的有用类。 从那时起,每个新的Java版本都增强了并发性API,甚至Java 8也提供了新的类和方法来处理并发。
接下来是executor
Executors
并发API将ExecutorService的概念引入为用于直接处理线程的替换。 Executors 能够运行异步任务,并且通常管理线程池,因此我们不必手动创建新线程。 内部池的所有线程将被重用在多任务下,所以我们可以在我们的应用程序的整个生命周期中使用单个执行器服务运行任意数量的并发任务。
这是第一个线程示例如何使用executors:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Hello " + threadName);
});
// => Hello pool-1-thread-1
类Executors提供了方便的工厂方法来创建不同类型的执行器服务。 在这个例子中,我们使用一个大小为1的线程池的executor 。
结果看起来类似于上面的示例,但是当运行代码时,你会注意到一个重要的区别:java进程永远不会停止! 执行者必须明确地停止 - 否则他们会继续监听新的任务。ExecutorService为此目的提供了两种方法:shutdown()等待当前运行的任务完成,而shutdownNow()中断所有正在运行的任务并立即关闭执行器。
这是我通常关闭执行程序的首选方式:
System.out.println("attempt to shutdown executor");
executor.shutdown();
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");
}
executor 通过等待一定量的时间来终止当前正在运行的任务而轻微地关闭。 最多5秒后,执行程序通过中断所有正在运行的任务最终关闭。
Callables and Futures
除了Runnable执行器支持另一种名为Callable的任务。 callables是功能接口,就像runnables一样,而不是void它们返回一个值。
这个lambda表达式定义了一个callable在休眠一秒后返回一个整数:
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(1);
return 123;
}
catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
};
Callables可以像运行一样提交给executor 服务。 但可调用的结果呢? 因为submit()不等待任务完成,executor service 不能直接返回可调用的结果。 相反,执行器返回类型为Future的特殊结果,可以用于在稍后的时间点检索实际结果。
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(task);
System.out.println("future done? " + future.isDone());
Integer result = future.get();
System.out.println("future done? " + future.isDone());
System.out.print("result: " + result);
在向可执行程序提交可调用程序后,我们首先通过isDone()检查未来是否已经执行完毕。
调用方法get()阻塞当前线程,并等待直到可调用方完成,然后返回实际结果123.现在的未来终于完成,我们在控制台上看到以下结果:
future done? false
future done? true
result: 123
Futures 与底层的executor service紧密耦合。 请记住,如果关闭执行executor,每个未终止的future 都会抛出异常:
executor.shutdownNow();
future.get();
您可能已经注意到,executor的创建与上一个示例略有不同。 我们使用newFixedThreadPool(1)创建一个由大小为1的线程池支持的executor service。 这相当于newSingleThreadExecutor(),但我们稍后可以通过传递大于1的值来增加池大小。
Timeouts
任何对future.get()的调用都将阻塞并等待,直到底层的callable被终止。 在最坏的情况下,callable永远运行 - 从而使您的应用程序无响应。 您可以通过传递超时来简单地抵消这些情况:
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(2);
return 123;
}
catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
});
future.get(1, TimeUnit.SECONDS);
Executing the above code results in a TimeoutException:
Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
您可能已经猜到为什么抛出此异常:我们指定了最大等待时间为1秒,但可调用实际上需要两秒钟才返回结果。
InvokeAll
Executors通过invokeAll()支持批量提交多个回调。 此方法接受可调用的集合并返回futures的列表。
ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<String>> callables = Arrays.asList(
() -> "task1",
() -> "task2",
() -> "task3");
executor.invokeAll(callables)
.stream()
.map(future -> {
try {
return future.get();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
})
.forEach(System.out::println);
在这个例子中,我们使用Java 8功能流来处理invokeAll的调用返回的所有futures 。 我们首先将每个futures 映射到其返回值,然后将每个值打印到控制台。 如果你还不熟悉流读取我的Java 8流教程。
InvokeAny
批量提交可调用的另一种方法是方法invokeAny(),它的工作方式与invokeAll()略有不同。 而不是返回未来的future ,此方法阻塞,直到第一个可调用方终止,并返回该可调用的结果。
为了测试这个行为,我们使用这个帮助方法来模拟不同持续时间的可调用。 该方法返回一个可调用,它在一定时间内休眠,直到返回给定结果:
Callable<String> callable(String result, long sleepSeconds) {
return () -> {
TimeUnit.SECONDS.sleep(sleepSeconds);
return result;
};
}
我们使用这种方法来创建一堆可调用的不同持续时间从一到三秒。 通过invokeAny()将这些可调用项提交给执行者,返回最快可调用的字符串结果 - 在这种情况下为task2:
ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<String>> callables = Arrays.asList(
callable("task1", 2),
callable("task2", 1),
callable("task3", 3));
String result = executor.invokeAny(callables);
System.out.println(result);
// => task2
我们使用这种方法来创建一堆可调用的不同持续时间从一到三秒。 通过invokeAny()将这些可调用项提交给执行者,返回最快可调用的字符串结果 - 在这种情况下为task2:
上面的例子使用了另一种类型的通过newWorkStealingPool()创建的执行器。 这个工厂方法是Java 8的一部分,并返回类型为ForkJoinPool的执行器,它的工作方式与正常执行器略有不同。 而不是使用固定大小的线程池ForkJoinPools是为给定的并行度大小创建的,默认情况下是主机CPU的可用核心数。
ForkJoinPools存在自Java 7,并将在本系列的后续教程中详细介绍。 让我们通过深入了解scheduled executors来完成本教程。
Scheduled Executors
我们已经学会了如何在执行器上提交和运行任务一次。 为了定期地多次运行常见任务,我们可以使用调度线程池。
ScheduledExecutorService能够调度任务定期运行或在经过一定时间后运行一次。
此代码示例计划任务在经过3秒的初始延迟后运行:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
ScheduledFuture<?> future = executor.schedule(task, 3, TimeUnit.SECONDS);
TimeUnit.MILLISECONDS.sleep(1337);
long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);
System.out.printf("Remaining Delay: %sms", remainingDelay);
计划任务生成类型ScheduledFuture - 除了Future - 提供方法getDelay()来检索剩余的延迟。 延迟过后,任务将同时执行。
为了计划要定期执行的任务,执行程序提供了两种方法scheduleAtFixedRate()和scheduleWithFixedDelay()。 第一种方法能够以固定的时间速率执行任务,例如, 每秒一次,如此示例中所示:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
int initialDelay = 0;
int period = 1;
executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);
此外,该方法接受描述在第一次执行任务之前的前导等待时间的初始延迟。
请记住,scheduleAtFixedRate()不会考虑任务的实际持续时间。 所以如果你指定一个周期为1秒,但任务需要2秒执行,线程池将很快工作容量。
在这种情况下,您应该考虑使用scheduleWithFixedDelay()。 此方法的工作方式与上述对应方法相同。 不同之处在于,等待时间段适用于任务结束和下一个任务的开始之间。 例如:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("Scheduling: " + System.nanoTime());
}
catch (InterruptedException e) {
System.err.println("task interrupted");
}
};
executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
此示例计划在执行结束和下一次执行开始之间具有固定延迟一秒的任务。 初始延迟为零,任务持续时间为2秒。 因此,我们最终的执行间隔为0s,3s,6s,9s等。 正如你可以看到scheduleWithFixedDelay()是方便,如果你不能预测计划任务的持续时间。