Asynchronous Updates
Environment.runLater() API 提供了一种机制,用于在 webforJ 应用程序中安全地从后台线程更新 UI。此实验性功能支持异步操作,同时保持 UI 修改的线程安全性。
The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:
- "Refresh this dashboard every 30 seconds."
- "Add a search-as-you-type debouncer."
- "Run this CPU-heavy work in the background and update the progress bar."
理解线程模型
webforJ 强制执行严格的线程模型,其中所有 UI 操作必须在 Environment 线程上进行。此限制的原因如下:
- webforJ API 限制:底层的 webforJ API 绑定到创建会话的线程
- 组件线程亲和性:UI 组件维护的状态不是线程安全的
- 事件分发:所有 UI 事件都是在单个线程上按顺序处理的
这种单线程模型防止竞争条件并保持所有 UI 组件的一致状态,但在与异步和长时间运行的计算任务集成时会带来挑战。
RunLater API
Environment.runLater() API 提供了两个用于调度 UI 更新的方法:
// 调度一个没有返回值的任务
public static PendingResult<Void> runLater(Runnable task)
// 调度一个返回值的任务
public static <T> PendingResult<T> runLater(Supplier<T> supplier)
这两个方法返回一个 PendingResult,用于跟踪任务完成情况并提供访问结果或发生的任何异常。
线程上下文继承
自动上下文继承是 Environment.runLater() 的一个关键特性。当在 Environment 中运行的线程创建子线程时,这些子 线程会自动继承使用 runLater() 的能力。
继承如何工作
从 Environment 线程内部创建的任何线程自动可以访问该 Environment。这种继承是自动发生的,因此您无需传递任何上下文或配置任何内容。
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// 该线程具有 Environment 上下文
// 子线程自动继承上下文
executor.submit(() -> {
String data = fetchRemoteData();
// 可以使用 runLater,因为上下文被继承了
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
没有上下文的线程
在 Environment 上下文之外创建的线程无法使用 runLater(),并会抛出 IllegalStateException:
// 静态初始化器 - 没有 Environment 上下文
static {
new Thread(() -> {
Environment.runLater(() -> {}); // 抛出 IllegalStateException
}).start();
}
// 系统计时器线程 - 没有 Environment 上下文
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // 抛出 IllegalStateException
}
}, 1000);
// 外部库线程 - 没有 Environment 上下文
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // 抛出 IllegalStateException
});
执行行为
runLater() 的执行行为取决于调用它的线程:
从 UI 线程
当从 Environment 线程本身调用时,任务 同步且立即 执行:
button.onClick(e -> {
System.out.println("之前: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("内部: " + Thread.currentThread().getName());
return "完成";
});
System.out.println("之后: " + result.isDone()); // true
});
通过这种同步行为,事件处理程序的 UI 更新立即应用,并不会产生不必要的排队开销。
从后台线程
当从后台线程调用时,任务会 排队进行异步执行:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// 这在 ForkJoinPool 线程上运行
System.out.println("后台: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// 这在 Environment 线程上运行
System.out.println("UI 更新: " + Thread.currentThread().getName());
statusLabel.setText("处理完成");
});
// result.isDone() 在这里将为 false
// 任务被排队,将异步执行
});
}
webforJ 以 严格的 FIFO 顺序 处理从后台线程提交的任务,保留操作的顺序,即使在多个线程同时提交时也是如此。通过这种排序保证,UI 更新按提交顺序应用。因此,如果线程 A 提交任务 1,然后线程 B 提交任务 2,任务 1 将始终在 UI 线程上执行任务 2 之前。FIFO 顺序处理任务可以防止 UI 中的不一致性。
任务取消
由 Environment.runLater() 返回的 PendingResult 支持取消,允许您防止排队任务的执行。通过取消待处理任务,您可以避免内存泄漏,并防止长时间运行的操作在不再需要时更新 UI。
基本取消
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// 如果尚未执行则取消
if (!result.isDone()) {
result.cancel();
}
管理多个更新
在执行长时间运行的操作时,如果需要频繁更新 UI,请跟踪所有待处理结果:
public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;
public void startTask() {
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;
final int progress = i;
PendingResult<Void> update = Environment.runLater(() -> {
progressBar.setValue(progress);
});
// 跟踪以便可能取消
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// 取消所有待处理的 UI 更新
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
组件生命周期管理
当组件被销毁 时(例如,在导航期间),取消所有待处理的更新以防止内存泄漏:
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// 取消所有待处理更新以防止内存泄漏
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
设计注意事项
-
上下文要求:线程必须继承
Environment上下文。外部库线程、系统计时器和静态初始化器无法使用此 API。 -
内存泄漏预防:始终跟踪和取消组件生命周期方法中的
PendingResult对象。排队的 lambda 会捕获对 UI 组件的引用,如果不取消,将阻止垃圾收集。 -
FIFO 执行:所有任务以严格的 FIFO 顺序执行,无论重要性如何。没有优先级系统。
-
取消限制:取消仅防止排队任务的执行。已经在执行的任务将正常完成。
完整案例研究:LongTaskView
以下是一个完整的、准备投入生产的实现,演示了异步 UI 更新的所有最佳实践:
案例研究分析
此实现展示了几个关键模式:
1. 线程池管理
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- 使用 单线程执行器 以防止资源耗尽
- 创建 守护线程,它们不会阻止 JVM 关闭
2. 跟踪待处理更新
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
每个 Environment.runLater() 调用都会被跟踪,以便于:
- 当用户点击取消时的取消
- 在
onDestroy()中防止内存泄 漏 - 在组件生命周期中的正确清理
3. 协作取消
private volatile boolean isCancelled = false;
后台线程在每次迭代中检查此标志,从而实现:
- 对取消的即时响应
- 从循环中干净地退出
- 防止进一步的 UI 更新
4. 生命周期管理
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // 复用取消逻辑
currentTask = null;
executor.shutdown();
}
通过以下方式防止内存泄漏至关重要:
- 取消所有待处理的 UI 更新
- 中断运行的线程
- 关闭执行器
5. UI 响应性测试
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("点击 #" + count + " - UI 是响应的!", Theme.GRAY);
});
演示了在后台操作期间 UI 线程保持响应。