跳至主要内容

异步更新

在 ChatGPT 中打开
25.02 Preview
Java API

Environment.runLater() API 提供了一种机制,可在 webforJ 应用中从后台线程安全地更新 UI。此实验性功能支持异步操作,同时确保 UI 修改的线程安全。

Preview feature
This feature is in preview. Its API may still change in future releases.
AI skill available

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 线程上执行。此限制的原因包括:

  1. webforJ API 的限制:底层 webforJ API 会绑定到创建会话的线程
  2. 组件的线程亲和性:UI 组件维护的状态并非线程安全
  3. 事件分发:所有 UI 事件都在单个线程上按顺序处理

这种单线程模型可以避免竞态条件,并确保所有 UI 组件的状态一致,但在集成异步的长时间运行计算任务时会带来挑战。

每个用户会话都有自己的 Environment 和对应的 Environment 线程。后台线程不属于任何会话,因此必须指定要更新的 Environment。

RunLater API​

Environment.runLater() API 提供了两种用于安排 UI 更新的方法。两种方法的第一个参数都是目标 Environment:

Environment.java
// Schedule a task with no return value
public static PendingResult<Void> runLater(Environment target, Runnable task)

// Schedule a task that returns a value
public static <T> PendingResult<T> runLater(Environment target, Supplier<T> supplier)

两种方法都会返回一个 PendingResult,用于跟踪任务完成状态,并提供访问结果或所发生异常的方式。

不带目标参数的重载

runLater(Runnable) 和 runLater(Supplier) 这两个重载会根据创建当前调用线程的线程来推测目标。如果线程池由多个用户共享,那么每个任务都会在创建该池线程的用户所对应的 Environment 中运行。这些重载自 26.10 起已弃用,并计划在下一个主要版本中移除。请始终传入目标参数。

传入目标​

Environment.getCurrent() 返回当前线程的 Environment。在 Environment 线程上(构造函数、事件处理程序和生命周期方法都在该线程上运行),它会返回会话的 Environment。在后台线程上,它会返回 null。

应在目标可用时,也就是后台工作开始之前读取目标,并将其传递给后台工作:

@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();

public DataView() {
// The constructor runs on the Environment thread
Environment env = Environment.getCurrent();

executor.submit(() -> {
String data = fetchRemoteData();

Environment.runLater(env, () -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

由于目标作为参数传入,因此可以从任何线程调用此方法:视图创建的线程、Spring @Async 线程、公共 ForkJoinPool、计时器线程,或 HTTP 客户端的回调线程。

在错误的线程上读取目标​

需要避免的错误是在后台工作中调用 Environment.getCurrent()。此时它会返回 null,导致 runLater() 抛出 NullPointerException:

executor.submit(() -> {
String data = fetchRemoteData();

// Wrong: getCurrent() returns null on a background thread
Environment.runLater(Environment.getCurrent(), () -> dataLabel.setText(data));
});

静态初始化器、java.util.Timer 线程和外部库回调也有同样的问题。它们都没有 Environment,因此目标必须来自字段、参数,或在 Environment 线程上赋值后捕获的局部变量。

执行行为​

runLater() 的执行行为取决于调用它的线程:

从 UI 线程调用​

如果从目标自身的 Environment 线程调用,任务会同步并立即执行:

button.onClick(e -> {
Environment env = Environment.getCurrent();
System.out.println("Before: " + Thread.currentThread().getName());

PendingResult<String> result = Environment.runLater(env, () -> {
System.out.println("Inside: " + Thread.currentThread().getName());
return "completed";
});

System.out.println("After: " + result.isDone()); // true
});

借助这种同步行为,事件处理程序中的 UI 更新会立即应用,不会产生不必要的排队开销。

从后台线程调用​

如果从任何其他线程调用,任务会排队等待异步执行:

@Override
public void onDidCreate() {
Environment env = Environment.getCurrent();

CompletableFuture.runAsync(() -> {
// This runs on a ForkJoinPool thread
System.out.println("Background: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(env, () -> {
// This runs on the Environment thread
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Processing complete");
});

// result.isDone() would be false here
// The task is queued and will execute asynchronously
});
}

webforJ 会严格按照 FIFO 顺序处理从后台线程提交的任务,即使这些任务是由多个线程并发提交的,也会保留其操作顺序。借助这一顺序保证,UI 更新会严格按照提交顺序应用。因此,如果线程 A 提交任务 1,随后线程 B 提交任务 2,那么任务 1 总会先于任务 2 在 UI 线程上执行。按 FIFO 顺序处理任务可以避免 UI 状态不一致。

取消任务​

Environment.runLater() 返回的 PendingResult 支持取消,因此可以阻止排队中的任务执行。取消待处理任务有助于避免内存泄漏,并防止不再需要的长时间运行操作继续更新 UI。

基本取消​

PendingResult<Void> result = Environment.runLater(env, () -> {
updateUI();
});

// Cancel if not yet executed
if (!result.isDone()) {
result.cancel();
}

管理多个更新​

执行长时间运行的操作并频繁更新 UI 时,应跟踪所有待处理结果:

public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;

public void startTask() {
Environment env = Environment.getCurrent();

CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;

final int progress = i;
PendingResult<Void> update = Environment.runLater(env, () -> {
progressBar.setValue(progress);
});

// Track for potential cancellation
pendingUpdates.add(update);

Thread.sleep(100);
}
});
}

public void cancelTask() {
isCancelled = true;

// Cancel all pending UI updates
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();

// Cancel all pending updates to prevent memory leaks
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

设计注意事项​

  1. 目标要求:在 Environment 线程上通过 Environment.getCurrent() 读取目标 Environment,并将其传递给后台工作。在后台线程上读取会返回 null。

  2. 防止内存泄漏:始终在组件生命周期方法中跟踪并取消 PendingResult 对象。排队中的 lambda 会捕获 UI 组件的引用;如果不取消,就会阻止垃圾回收。

  3. FIFO 执行:所有任务都严格按照 FIFO 顺序执行,不考虑任务的重要性。此处没有优先级系统。

  4. 取消的限制:取消只能阻止尚在排队中的任务执行。已经开始执行的任务会正常完成。

完整案例研究:LongTaskView​

下面是一个完整且可用于生产环境的实现,展示了异步 UI 更新的所有最佳实践:

LongTaskView.java
startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Starting background task...");
progressBar.setValue(0);
resultField.setValue("");

// Reset cancelled flag and clear previous pending updates
isCancelled = false;
pendingUIUpdates.clear();

// Read the target Environment on the UI thread, before the background task starts
Environment env = Environment.getCurrent();

// Start background task with explicit executor
// Note: cancel(true) will interrupt the thread, causing Thread.sleep() to throw
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simulate long task with 100 steps
for (int i = 0; i <= 100; i++) {
// Check if cancelled
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(env, () -> {
statusField.setValue("Task cancelled!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Task was cancelled", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 seconds total
} catch (InterruptedException e) {
// Thread was interrupted - exit immediately
Thread.currentThread().interrupt(); // Restore interrupted status
return;
}

// Do some calculation (deterministic for demo)
// Produces values between 0 and 1
result += Math.sin(i) * 0.5 + 0.5;

// Update progress from background thread
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(env, () -> {
progressBar.setValue(progress);
statusField.setValue("Processing... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Final update with result (this code is only reached if the task completed without
// cancellation)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(env, () -> {
statusField.setValue("Task completed!");
resultField.setValue("Result: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Background task finished!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

案例分析​

此实现展示了几个关键模式:

1. 线程池管理​

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • 使用单线程执行器,避免资源耗尽
  • 创建守护线程,使其不会阻止 JVM 关闭

2. 指定目标​

Environment env = Environment.getCurrent();

startLongTask() 在 Environment 线程上运行,因此它会在该线程上读取目标,并由后台任务将目标传递给每次 Environment.runLater() 调用。相同代码也可以直接用于共享的应用级线程池。

3. 跟踪待处理的更新​

private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();

跟踪每次 Environment.runLater() 调用,以便:

  • 用户点击取消时取消任务
  • 在 onDestroy() 中防止内存泄漏
  • 在组件生命周期内执行适当的清理

4. 协作式取消​

private volatile boolean isCancelled = false;

后台线程会在每次迭代时检查此标志,从而实现:

  • 立即响应取消请求
  • 正常退出循环
  • 防止继续更新 UI

5. 生命周期管理​

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reuses cancellation logic
currentTask = null;
executor.shutdown();
}

这是防止内存泄漏的关键措施,具体包括:

  • 取消所有待处理的 UI 更新
  • 中断正在运行的线程
  • 关闭执行器

6. UI 响应性测试​

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Click #" + count + " - UI is responsive!", Theme.GRAY);
});

此示例展示了在后台操作期间,UI 线程仍能保持响应。

继续学习