3

我已经定义了一个具有许多“可观察”属性的类。该类在内部包含一个执行 I/O 的线程;例如

public class Foo {
  private final PropertyChangeSupport support;
  private State state;

  public Foo() { this.support = new PropertyChangeSupport(this); }

  public synchronized State getState() { return state; }

  public synchronized void setState(State state) {
    if (this.state != state) {
      State oldState = this.state;
      this.state = state;

      // Fire property change *whilst still holding the lock*.
      support.firePropertyChange("state", oldState, state);
    }
  }

  public synchronized void start() {
    // Start I/O Thread, which will call setState(State) in some circumstances.
    new Thread(new Runnable() ...
  }
}

我的问题是:我应该避免在持有类锁的同时触发属性更改事件吗?...或者也许我应该从单个专用线程(例如“事件多播”线程)触发属性更改事件?

当前的设计导致死锁,线程 A 在外部类中取出锁Bar,然后尝试调用方法Foo并取出第二个锁。但是,同时 I/O 线程调用setState(State)获取 lock on Foo,这会将属性更改事件传播到包含类Bar并尝试获取此类上的锁......导致死锁。换句话说,属性更改回调设计意味着我无法有效控制获取锁的顺序。

我目前的解决方法是创建状态volatile并删除synchronized关键字,但这似乎是一个杂项;一方面,这意味着无法保证触发属性更改事件的顺序。

4

1 回答 1

3

如果您需要让其他线程从通知循环回调到您的类中,那么您需要减少同步范围,使用同步块而不是同步整个消息(这是从您的帖子中复制的,不知道它是否编译):

public void setState(State state) {
    State oldState = null;
    synchronized (this) {
      if (this.state != state) {
        oldState = this.state;
        this.state = state;
      }
    }

    if (oldState != null)
      support.firePropertyChange("state", oldState, state);
  }

尽可能缩短锁定时间。并考虑用同步的消息队列替换这些回调(查看 java.util.concurrent)。


编辑,概括答案并添加一些哲学。

首先咆哮:多线程编程很难。任何告诉你不同的人都试图向你推销一些东西。在多线程程序中创建大量相互依赖的连接会导致微妙的错误,如果不是彻底的疯狂的话。

我所知道的简化多线程编程的最好方法是编写具有明确定义的起点和终点的独立模块——换句话说,就是actor模型。单个actor是单线程的;您可以轻松地单独理解和测试它。

纯actor模型非常适合事件通知:actor被调用以响应事件,并执行一些动作。它不关心自开始以来是否触发了另一个事件。有时,这还不够:您需要根据另一个线程管理的状态做出决定。没关系:参与者可以查看该状态(以同步方式)并做出决定。

要记住的关键点(正如 Tom Hawtin 在他的评论中指出的那样)是您现在读取的状态可能与现在的毫秒不同。您永远无法编写假定您知道对象的确切状态的代码。如果你觉得你需要这样做,你需要重新考虑你的设计。

最后的评论:Doug Lea 比你我聪明。不要试图重新发明java.util.concurrent 中的类。

于 2009-09-24T11:14:59.830 回答