前言
由于多核系统普遍存在,并发性编程的应用无疑比以往任何时候都要广泛。但并发性很难正确实现,用户需要借助新工具来使用它。很多基于 JVM 的语言都属于这类开发工具,Scala 在这一领域尤为活跃。本系列文章将介绍一些针对 Java 和 Scala 语言的较新的并发性编程方法。
在任何并发性应用程序中,异步事件处理都至关重要。事件来源可能是不同的计算任务、I/O 操作或与外部系统的交互。无论来源是什么,应用程序代码都必须跟踪事件,协调为响应事件而采取的操作。
Java 应用程序可采用两种基本的异步事件处理方法:该应用程序有一个协调线程等待事件,然后采取操作,或者事件可在完成时直接执行某项操作(通常采取执行应用程序所提供的代码的方式)。让线程等待事件的方法被称为阻塞 方法。让事件执行操作、线程无需显式等待事件的方法被称为非阻塞 方法。
在计算中,根据具体上下文,阻塞 和非阻塞 这两个词的使用通常会有所不同。举例而言,共享数据结构的非阻塞算法不需要线程等待访问数据结构。在非阻塞 I/O 中,应用程序线程可以启动一个 I/O 操作,然后离开执行其他事情,同时该操作会异步地执行。在本文中,非阻塞 指的是在无需等待线程的情况下完成某个执行操作的事件。这些用法中的一个共同概念是,阻塞操作需要一个线程来等待某个结果,而非阻塞操作不需要。
合成事件
等待事件的完成很简单:您有一个线程等待该事件,线程恢复运行时,您就可以知道该事件已经完成。如果您的线程在此期间有其他事要做,它会做完这些事再等待。该线程甚至可以使用轮询方法,通过该方法中断它的其他活动,从而检查事件是否已完成。但基本原理是相同的:需要事件的结果时,您会让线程停靠 (park),以便等待事件完成。
阻塞很容易完成且相对简单,只要您有一个等待事件完成的单一主线程。使用多个因为彼此等待而阻塞的线程时,可能遇到一些问题,比如:
死锁:两个或更多线程分别控制其他线程继续执行所需的资源。
饥饿 (Starvation):一些线程可能无法继续执行,因为其他线程贪婪地消耗着共享资源。
活锁:线程尝试针对彼此而调整,但最终没有进展。
非阻塞方法为创造力留出的空间要多得多。回调是非阻塞事件处理的一种常见技术。回调是灵活性的象征,因为您可以在发生事件时执行任何想要的代码。回调的缺点是,在使用回调处理许多事件时,您的代码会变得凌乱。而且回调特别难调试,因为控制流与应用程序中的代码顺序不匹配。
Java 8 CompletableFuture 同时支持阻塞和非阻塞的事件处理方法,包括常规回调。CompletableFuture 也提供了多种合成和组合事件的方式,实现了回调的灵活性以及干净、简单、可读的代码。在本节中,您将看到处理由 CompletableFuture 表示的事件的阻塞和非阻塞方法的示例。
任务和排序
应用程序在一个特定操作中通常必须执行多个处理步骤。例如,在向用户返回结果之前,Web 应用程序可能需要:
1.在一个数据库中查找用户的信息
2.使用查找到的信息来执行 Web 服务调用,并执行另一次数据库查询。
3.基于来自上一步的结果而执行数据库更新。
图 1 演示了这种结构类型。
图 1. 应用程序任务流
图 1 将处理过程分解为 4 个不同的任务,它们通过表示顺序依赖关系的箭头相连接。任务 1 可直接执行,任务 2 和任务 3 都在任务 1 完成后执行,任务 4 在任务 2 和任务 3 都完成后执行。这是我在本文中用于演示异步事件处理的任务结构。真实应用程序(尤其是具有多个移动部分的服务器应用程序)可能要复杂得多,但这个简单的示例仅用于演示所涉及的原理。
建模异步事件
在真实系统中,异步事件的来源一般是并行计算或某种形式的 I/O 操作。但是,使用简单的时间延迟来建模这种系统会更容易,这也是本文所采用的方法。清单 1 显示了我用于生成事件的基本的赋时事件 (timed-event) 代码,这些事件采用了 CompletableFuture 格式。
清单 1. 赋时事件代码
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;
public class TimedEventSupport {
private static final Timer timer = new Timer();
/**
* Build a future to return the value after a delay.
*
* @param delay
* @param value
* @