【问题标题】:Why does adding this defensive copy avoid deadlocks?为什么添加这个防御性副本可以避免死锁?
【发布时间】:2018-06-14 17:57:26
【问题描述】:

这是原始代码。这个程序最终可能会死锁,因为updateProgress 方法调用了另一个可能会或可能不会获得另一个锁的方法。而且我们在不知道是否以正确的顺序完成的情况下获得了这两个锁。

import java.io.*;
import java.net.URL;
import java.util.ArrayList;

class Downloader extends Thread {
  private InputStream in;
  private OutputStream out;
  private ArrayList<ProgressListener> listeners;

  public Downloader(URL url, String outputFilename) throws IOException {
    in = url.openConnection().getInputStream();
    out = new FileOutputStream(outputFilename);
    listeners = new ArrayList<ProgressListener>();
  }
  public synchronized void addListener(ProgressListener listener) {
    listeners.add(listener);
  }
  public synchronized void removeListener(ProgressListener listener) {
    listeners.remove(listener);
  }
  private synchronized void updateProgress(int n) {
    for (ProgressListener listener: listeners)
      listener.onProgress(n);
  }

  public void run() {
    int n = 0, total = 0;
    byte[] buffer = new byte[1024];

    try {
      while((n = in.read(buffer)) != -1) {
        out.write(buffer, 0, n);
        total += n;
        updateProgress(total);
      }
      out.flush();
    } catch (IOException e) { }
  }
}

教科书的作者建议在迭代之前更改updateProgress 以创建ArrayList&lt;ProgressListener&gt; listeners 的防御性副本。

private void updateProgress(int n) {
    ArrayList<ProgressListener> listenersCopy;
    synchronized(this) {
      listenersCopy = (ArrayList<ProgressListener>)listeners.clone();
    }
    for (ProgressListener listener: listenersCopy)
      listener.onProgress(n);

}

这样做可以避免在持有锁的情况下调用“外星人”方法,并减少在updateProgress 中获取的原始锁被持有的时间。我理解为什么它会减少持有锁的时间,但不理解它如何避免在持有锁的情况下调用外星方法。这是我的思路。

  1. 它创建数组列表listeners 的克隆。此克隆是一个单独的对象,其中包含原始 listener 所具有的确切元素。

  2. 现在这是线程安全的,因为现在您有一个“本地”副本,至少对于该特定线程来说是本地的,并且另一个线程对其本地副本所做的事情不会影响您。

    李>
  3. 您通过onProgress 方法更新侦听器。但是,此更改仅适用于您的 listeners 副本。

  4. updateProgress 返回但“本地”更改如何传播到“原始”listeners?由于它是一个克隆,它们是独立的对象,但它们如何将它们的更新相互通信?

这就是我坚持的部分。

【问题讨论】:

  • 旁白:CopyOnWriteArrayList 在这里可能会更好。
  • 我认为这一切都是为了减轻其中一位听众在其onProgress 实施期间调用addListenerremoveListener 的病态情况。
  • @OliverCharlesworth 但肯定这与死锁没有任何关系,而是会导致ConcurrentModificationException
  • @AndyTurner - 我认为原始代码死锁。通过输入updateProgress获取锁,然后调用listener.onProgress,然后(病态地)调用addListener,尝试获取锁。
  • 哦,除了synchronized 方法是可重入的。所以我不明白这里到底会出现什么问题。

标签: java multithreading concurrency


【解决方案1】:

一个真正病态的情况是:

  • 其中一位听众开始Thread
  • 该线程尝试在Downloader 上调用同步方法
  • 调用侦听器的线程(启动新线程的线程)在启动的线程上调用join

类似:

class Pathological implements ProgressListener {
  // Initialize in ctor.
  final Downloader downloader;

  @Override void onProgress(int n) {
    Thread t = new Thread(() -> downloader.removeListener(Pathological.this));
    t.start();
    t.join();
  }
}

在这种情况下你会遇到死锁,因为当第一个线程持有监视器时,启动的线程无法取得进展。

采用防御性副本可以避免这种情况,因为在调用Pathological.onProgress 时第一个线程没有持有监视器;但我仍然更喜欢使用旨在处理并发访问的替代列表实现,例如CopyOnWriteArrayList.

【讨论】:

  • 比我想象的还要病态 ;)
  • @OliverCharlesworth 我相信唯一合适的回应是“muhahahahaha”。
  • 或者使用multicaster模式,在监听器数量较少的情况下效率特别高。
【解决方案2】:

您问题的标题与最后的问题不匹配。问题的标题是关于避免死锁,已经是answered by Andy Turner。他描述了一个场景,死锁是如何发生的,并且你已经在问题中给出了结论:你通过在持有锁时不调用“外来方法”来避免死锁。

既然这似乎已经被理解,你的问题实际上是一个完全不同的问题。您是在问,“‘本地’更改如何传播到‘原始’侦听器?”

答案是,没有这样的局部变化。您列表的第三个项目符号是错误的。本地副本不仅对于当前线程是本地的,对于 updateProgress 方法也是本地的,并且不会被任何其他代码看到。

所以当通过addListenerremoveListener对监听器列表进行修改时,无论是哪个线程进行修改,都会直接影响到对象的listener字段引用的列表,它仍然是和以前一样。但是这个改动不影响本地副本,updateProgress 方法正在迭代。由于updateProgress 只是迭代本地副本,因此它永远不会有任何需要传播到原始列表的修改。

请注意,在单线程方案中甚至需要此副本。许多List 实现,尤其是ArrayList,不支持在有人对其进行迭代时对其进行修改,即使修改是由执行迭代的同一线程进行的(通过相同的@987654333 修改列表除外@ 用于迭代,此处不适用)。

替代方案是CopyOnWriteArrayList,它在迭代时不需要复制,但是当列表被修改时,或者Multicaster模式,它在删除监听器时可能只需要(部分)复制,但当数量减少时可能会变得低效的听众变得很大(但对于少数人来说非常效率)。请参阅 AWTEventMulticaster 以获取此模式的示例 implementation

【讨论】:

    猜你喜欢
    • 2011-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-17
    相关资源
    最近更新 更多