使用PropertyChangeListener的锁定策略

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)来获取Foo上的锁,这会将属性更改事件传播到包含类Bar并尝试获取此类的锁... 从而导致死锁。换句话说,属性更改回调设计意味着我无法有效地控制获取锁的顺序。
我的当前解决方法是使状态volatile并删除synchronized关键字,但这似乎不太好;首先,这意味着不能保证触发属性更改事件的顺序。
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 model。单个 actor 是单线程的;您可以轻松地理解和测试它。
纯 actor 模型非常适合事件通知:actor 在响应事件时被调用,并执行某些操作。它不关心自启动以来是否已触发了另一个事件。有时这还不够:您需要基于由另一个线程管理的状态做出决策。没关系:actor 可以查看该状态(以同步方式)并做出决策。
要记住的关键一点(正如 Tom Hawtin 在他的评论中指出的那样)是,您现在读取的状态可能在一毫秒后就变了。您永远不能编写假定您知道对象的确切状态的代码。如果您觉得需要这样做,那么您需要重新考虑您的设计。

最后一句话:Doug Lea 比你或我都聪明。不要试图重新发明 java.util.concurrent 中的类。


这绝对是一种改进,但不能保证属性更改事件按正确的顺序触发。 - Adamski
1
欢迎来到多线程编程。这就是为什么我说更好的方法是使用消息队列作为中介的原因。 - kdgregory
谢谢 - 我明白你的观点。我想这与我提到为触发PropertyChangeEvents而有一个专用线程有关。专用线程可以从队列中读取和传播,因此API不会改变。这也更好,因为我的I/O线程从未离开过我的类...但是当接收到事件时,不能保证变量仍具有该值,这一点不太好 :-( - Adamski
如果当前变量的状态很重要,那么你需要使用轮询机制。如果变量的改变很重要,那么你可以使用通知机制。 - kdgregory
当然,使用轮询方式获取值后,该值可能会立即发生更改。 - Tom Hawtin - tackline

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接