【问题标题】:JavaFX beans/properites and multithreadingJavaFX bean/properites 和多线程
【发布时间】:2016-01-08 21:50:24
【问题描述】:

我必须说我是 Java 和 JavaFX 的新手(不到 2 个月),而且我的 UML 技能并不完美,所以我可能在图表上使用了错误的块或/和连接器 :) 但我希望你有一个想法。

我有一个订单管理应用,其设计如下(简化为必需品):

OMS app design

简单地说,JavaFX GUI 在表格视图中显示客户端(发送订单)和代理(在网络连接的另一端)之间发生的事情(即订单的当前状态)。 Order Manager 是唯一有权修改模型(即订单列表及其字段)的实体,并且修改模型的所有方法都是同步的(因此从任何线程调用它们都是安全的)。

订单是 JavaFX bean,不同的字段实现为 JavaFX 属性。这些属性绑定到 GUI 中的表列,因此每当订单管理器更新字段时,更改都会通过绑定机制传播到 GUI。

现在,因为属性绑定机制不是线程安全的(参见下面的rule

应用程序必须将节点附加到场景,并在 JavaFX 应用程序线程上修改已附加到场景的节点。

),我必须在 Platform.runLater() 调用中包装所有修改这些字段的代码,例如:

public void onOrderCanceled(int id, String reason) {
    Order order = orderbook.get(id);
    if(order == null) {
        throw new IllegalArgumentException("Order "+id+ " not found");
    }
    Platform.runLater(() -> {
        order.setReason(reason);
        order.setStated(CANCELED);

        subscribers.foreach(sub -> sub.notifyUpdated(order));
    });
}

这种方法有以下不愉快的影响:

  1. 客户端通知延迟任意时间(直到 GUI 线程完成其消息队列的处理)。原因:订单字段更新前无法通知客户端(否则数据不正确),只能在GUI线程中更新字段。
  2. 由于订单状态不会立即修改,而是在未来某个时间点,订单对象与实际订单状态之间存在一段时间的不一致。
  3. 如果 GUI 线程被阻塞或变得非常慢(由于错误或设计缺陷),客户端代码会被阻塞或变慢(在等待 GUI 线程的消息队列中卡住的通知时)。​​

有没有更好的方法呢?理想情况下,我想要一个解决方案:

  • 允许客户端代码尽可能快地与网络层通信(通过订单管理器),即无需等待 GUI 跟上
  • 允许 GUI 稍有滞后,但不得“跳过”字段更新,或者至少永远不要放弃最近的更新(这是最相关的)
  • 依靠FX属性绑定架构更新GUI(我觉得很方便)

我觉得我需要为仅在 FX 线程中更新的 GUI 创建另一个“模型”,而“真实”模型将由订单管理器和客户端代码使用,我需要确保保持两个模型同步(这令人毛骨悚然)。

FX 的设计是否考虑了多线程?我查看了任务和服务接口,但看起来不合适(在我的情况下,GUI 不会启动任务 - 它来自外部源、客户端代码或网络)。

提前致谢!

【问题讨论】:

  • 您能解释一下 Task 或 Service 类没有帮助吗?
  • @JSamir 因为 Task 旨在为在后台运行并返回结果的长异步操作建模。就我而言,该应用程序大部分时间只是坐着等待外部事件。请注意,与代理的通信不是严格的请求-响应,可能存在没有任何请求的“响应”(通知)。我可能很笨,但我只是不明白如何在这里应用它,将什么封装为任务......
  • 当你说应用正在等待外部事件时,这些外部事件和你的应用是如何交互的?
  • @ItachiUchiha 有两种类型的事件:来自代理的消息(由最终调用订单管理器方法的专用网络线程处理)和来自客户端代码的操作(它在单独的线程中运行,并在满足某些条件时调用订单管理器的方法)
  • sub.notifyUpdated(order); 是做什么的?我猜它正在通知客户。据我了解,您实际上并不需要在 UI 线程上执行此操作,您只需要确保在 UI 上发生相同的更新,并且以相同的顺序,对吗?

标签: java multithreading user-interface javafx


【解决方案1】:

如果我正确理解了这个实现,那么您就是在 FX 应用程序线程上向远程客户端执行通知。您这样做是因为您需要向客户端发送 Order 对象的更新版本,并且由于该对象已绑定到表,因此对其状态的更改只能在该线程上发生。

这有点危险,因为这些远程通知可能需要时间,因此您可能会阻塞 UI 线程,从而导致缺乏响应能力。此外,您正在强制应用程序的逻辑等待(可能被阻塞的)UI 线程。这与您应该做的事情相反:您的应用程序逻辑应该以自然的方式在执行线程中流动,并且您应该安排 FX 应用程序线程以轻量级的方式显示最新版本的数据在每一帧渲染上尽可能多的一种方式。

我认为您需要的是一个单独的、不可变的类来表示更新。您将这些发送给您的客户,然后在成功通知时更新 UI,而不是从 UI 驱动应用程序。所以像

public abstract class Update {

    private final int orderId ;

    public Update(int orderId) {
        this.orderId = orderId ;
    }

    public abstract void performUpdate(Order order) ;

    public int getOrderId() {
        return orderId ;
    }
}

public class Cancelation extends Update {

    private final String reason ;

    public Cancelation(int orderId, String reason) {
        super(orderId);
        this.reason = reason ;
    }

    public String getReason() {
        return reason ;
    }

    @Override
    public void performUpdate(Order order) {
        if (order.getId() != getOrderId()) {
            throw new IllegalArgumentException("Wrong order");
        }
        order.setReason(reason);
        order.setState(CANCELED);
    }
}

现在在您的应用程序代码中,您可以执行类似的操作

public void onOrderCanceled(int id, String reason) {
    Order order = orderbook.get(id);
    if(order == null) {
        throw new IllegalArgumentException("Order "+id+ " not found");
    }
    Task<Update> updateTask = new Task<Update>() {
        @Override
        public Update call() throws Exception {
            Update update = new Cancelation(id, reason);
            subscribers.forEach(sub -> sub.notifyUpdate(update));
            return update ;
        }
    };
    updateTask.setOnSucceeded(e -> updateTask.getValue().performUpdate(order));
    updateTask.setOnFailed(e -> {
        Exception exc = updateTask.getException();
        // handle exception
    });
    subscriberNotification.execute(updateTask);
}

现在您可以在专用于该特定功能的执行程序中安排更新,然后在您知道通知已发生时更新 UI。换句话说,UI 响应应用程序逻辑,而不是相反。您的客户现在会收到更新的详细信息,并且可能他们有自己的Order 表示(可能是同一类的实例),并且可以使用Update 中的信息更新他们自己的表示。请注意,这也可能会节省您的网络流量(可能是您的应用程序中最大的瓶颈),因为您只传达更改,而不是整个对象。当然,这可能需要对您的代码进行一些重大重构(对此感到抱歉...)。

更新都以正确的顺序传输很重要,因此您应该使用单个执行线程来管理它们。换句话说,你需要类似的东西

private final Executor subscriberNotification = Executors.newSingleThreadedExecutor();

【讨论】:

  • 谢谢!如果我的客户是远程的,我肯定会听从您的建议,但是(到目前为止)它们是作为同一个应用程序的一部分运行的(抱歉,对此不够清楚)。这意味着它们可能会保留对 Order 对象的引用(具有只读访问权限)。我想避免的是在程序的不同部分(在订单管理器和客户端代码中)有相同实体(即订单)的不同表示。
  • 如果他们引用了同一个对象,那么你为什么需要notifyUpdated 调用呢?他们不能只观察 JavaFX 属性吗?
  • 嗯,他们确实可以。假设这个回调只是一种方便的方法(避免他们监听 10 多个属性并从各个字段的变化中弄清楚发生了什么)。好吧,我可以摆脱它。但这并不能解决最初的问题,即通知触发(修改属性)发生在 GUI 线程中,是吗?
  • 嗯。在那种情况下,我真的不认为有什么问题需要担心。只要您确保代码的一致性,并且只要您不在 UI 线程上执行任何长时间运行的操作(听起来您不这样做),那么我认为您没问题。我想唯一值得关注的问题是,如果您调度 Platform.runLater() 任务的速度超过了它们的执行速度,但除非您正在做一些计算成本很高的事情,否则情况不应该如此。
  • 嗯,我主要担心的是,由于我是 GUI 编程和 JavaFX 的新手,我可以在 GUI 线程中做一些愚蠢的事情,这会消耗大量 CPU 并且会减慢不完全关心 GUI...你部分地让我放心,谢谢你:) 但是对我来说,必须在 GUI 线程上更新模型并让所有用户等到它完成仍然有点违反直觉......
猜你喜欢
  • 1970-01-01
  • 2020-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-18
  • 1970-01-01
  • 1970-01-01
  • 2013-04-08
相关资源
最近更新 更多