为什么添加这个防御性副本可以避免死锁?

时间:2018-01-04 23:01:41

标签: java multithreading concurrency

这是原始代码。此程序可能最终导致死锁,因为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<ProgressListener> listeners的防御副本。

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

}

这样做可以避免打电话给&#34;外星人&#34;保持锁定的方法,它减少了在updateProgress中获取的原始锁定的时间。我理解为什么它会减少锁定的时间,但不能避免它如何避免使用锁定来调用异类方法。这是我的思路。

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

  2. 这是线程安全的,因为现在你有一个&#34; local&#34;复制,至少是该特定线程的本地,以及另一个线程对其本地副本的影响不会对您产生影响。

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

  4. updateProgress会返回,但&#34; local&#34;变化传播到&#34;原始&#34; listeners?既然它是一个克隆版,它们会分开对象,但是它们如何将它们的更新相互通信?

  5. 那是我坚持的部分。

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:0)

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

由于这似乎已经被理解,你的问题实际上是完全不同的。你问,“'本地'变化如何传播到'原始'听众?”

答案是,没有这样的局部变化。列表的第3个子弹是错误的。本地副本不仅是当前线程的本地副本,副本是updateProgress方法的本地副本,而且任何其他代码都看不到。

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

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

备选方案是CopyOnWriteArrayList,它在迭代时不需要复制,但是当列表被修改时,或者在删除侦听器时可能只需要(部分)复制的Multicaster模式,但是当它可能变得低效时侦听器的数量变大(但对于小数字,非常有效)。有关此模式的示例AWTEventMulticaster,请参阅implementation