跳至主要内容

Asynchronous Updates

在 ChatGPT 中打开
25.02 实验性
Java API

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

实验性功能
此功能为实验性,未来版本可能会改变或删除。
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 强制执行严格的线程模型,所有用户界面操作必须在 Environment 线程上进行。之所以存在这种限制,是因为:

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

这种单线程模型防止竞争条件,并为所有用户界面组件维护一致的状态,但在与异步的、长时间运行的计算任务集成时则会带来挑战。

RunLater API

Environment.runLater() API 提供了两种调度用户界面更新的方法:

Environment.java
// 调度一个没有返回值的任务
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() 的执行行为取决于调用它的线程:

从用户界面线程

当从 Environment 线程本身调用时,任务 同步并立即 执行:

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

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

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

通过这种同步行为,事件处理程序中的用户界面更新立即应用,并且不会产生任何不必要的排队开销。

从后台线程

当从后台线程调用时,任务 排队以进行异步执行

@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// 这在 ForkJoinPool 线程上运行
System.out.println("Background: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(() -> {
// 这在 Environment 线程上运行
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Processing complete");
});

// result.isDone() 在这里将是 false
// 任务已排队,将异步执行
});
}

webforJ 以 严格的 FIFO 顺序 处理来自后台线程提交的任务,在多个线程并发提交时仍然保持操作顺序。通过这种排序保证,用户界面的更新将以提交的确切顺序应用。如果线程 A 提交任务 1,然后线程 B 提交任务 2,则任务 1 始终会在用户界面线程上执行,然后才是任务 2。按 FIFO 顺序处理任务可以防止用户界面中的不一致性。

任务取消

Environment.runLater() 返回的 PendingResult 支持取消,允许您防止排队任务执行。通过取消待处理任务,您可以避免内存泄漏,并防止长时间运行的操作在不再需要时更新用户界面。

基本取消

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

// 如果尚未执行,取消
if (!result.isDone()) {
result.cancel();
}

管理多个更新

在进行需要频繁用户界面更新的长时间运行的操作时,跟踪所有待处理结果:

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;

// 取消所有待处理的用户界面更新
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();
}
}

设计考虑

  1. 上下文要求:线程必须继承 Environment 上下文。外部库线程、系统计时器和静态初始化器无法使用此 API。

  2. 内存泄漏预防:始终在组件生命周期方法中跟踪和取消 PendingResult 对象。排队的 lambda 捕获对用户界面组件的引用,如果不取消,将阻止垃圾收集。

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

  4. 取消限制:取消仅阻止排队任务的执行。已经在执行的任务将正常完成。

完整案例研究:LongTaskView

以下是一个完整且生产就绪的实现,展示了异步用户界面更新的所有最佳实践:

LongTaskView.java
cancelButton.setEnabled(true);
statusField.setValue("正在启动背景任务...");
progressBar.setValue(0);
resultField.setValue("");

// 重置取消标志并清除先前的待处理更新
isCancelled = false;
pendingUIUpdates.clear();

// 启动带有显式执行器的后台任务
// 注意:cancel(true) 将中断线程,导致 Thread.sleep() 抛出
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// 模拟长任务,分为 100 步
for (int i = 0; i <= 100; i++) {
// 检查是否取消
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("任务已取消!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("任务已取消", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 总共 10 秒
} catch (InterruptedException e) {
// 线程被中断 - 立即退出
Thread.currentThread().interrupt(); // 恢复中断状态
return;
}

// 执行某些计算(为演示目的而确定的)
// 产生 0 到 1 之间的值
result += Math.sin(i) * 0.5 + 0.5;

// 从后台线程更新进度
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("处理... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// 最终更新结果(此代码仅在任务未被取消的情况下到达)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("任务完成!");
resultField.setValue("结果:" + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("后台任务已完成!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

private void cancelTask() {
if (currentTask != null && !currentTask.isDone()) {

案例研究分析

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

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;

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

  • 对取消的即时响应
  • 从循环中清理退出
  • 防止进一步的用户界面更新

4. 生命周期管理

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // 重用取消逻辑
currentTask = null;
executor.shutdown();
}

通过以下方式,防止内存泄漏:

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

5. 用户界面响应性测试

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("点击 #" + count + " - 用户界面响应顺畅!", Theme.GRAY);
});

演示用户界面线程在后台操作期间保持响应。