Java 为什么添加此防御副本可以避免死锁?
这是原始代码。由于Java 为什么添加此防御副本可以避免死锁?,java,multithreading,concurrency,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
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) { }
}
}
}
这样做可以避免调用持有锁的“alien”方法,并减少在updateProgress
中获取的原始锁被持有的时间。我理解为什么它减少了锁的持有时间,但不理解它如何避免调用持有锁的外来方法。这是我的思路
侦听器的克隆。此克隆是一个单独的对象,它包含原始侦听器所具有的确切元素
onProgress
方法更新侦听器。但是,此更改仅适用于您的侦听器
副本updateProgress
返回但“本地”更改如何传播到“原始”侦听器?因为它是一个克隆,所以它们是独立的对象,但是它们如何相互通信更新
这就是我一直坚持的部分。一个真正的病理病例应该是:
- 其中一个侦听器启动一个
线程
- 该线程试图调用
下载程序上的同步方法
- 调用侦听器的线程(启动该新线程的线程)在启动的线程上调用
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();
}
}
在这种情况下会出现死锁,因为当第一个线程持有监视器时,启动的线程无法前进
获取防御副本可以避免这种情况,因为调用
physical.onProgress
时,第一个线程没有占用监视器;但我还是更愿意使用一种替代列表实现来处理并发访问,例如,CopyOnWriteArrayList
问题的标题与最后的问题不匹配。这个问题的标题是关于避免僵局,这一点已经得到了解决。他描述了一个场景,死锁是如何发生的,并且你自己在这个问题中给出了一个结论:在持有锁时,你不调用“外来方法”来避免死锁
因为这似乎已经被理解了,你的问题实际上是一个完全不同的问题。您会问,“如何将“本地”更改传播到“原始”侦听器?”
答案是,没有这样的局部变化。你名单上的第三个项目是错误的。本地副本不仅是当前线程的本地副本,而且是updateProgress
方法的本地副本,任何其他代码都看不到
因此,当通过addListener
或removeListener
对侦听器列表进行修改时,无论是哪个线程进行修改,都会直接影响对象的listener
字段引用的列表,该字段仍然与以前相同。但是此更改不会影响本地副本,updateProgress
方法正在迭代。由于updateProgress
只在本地副本上迭代,因此它永远不会有任何需要传播到原始列表的修改
请注意,在单线程场景中甚至需要此副本。许多列表
实现,尤其是ArrayList
,不支持在有人对其进行迭代时进行修改,即使修改是由执行迭代的同一线程进行的(除了通过用于迭代的相同迭代器
修改列表,此处不适用)
替代方案有:在迭代时不需要复制,但在修改列表时需要复制;或者是多播模式,在删除侦听器时可能只需要(部分)复制,但在侦听器数量变大时可能会变得效率低下(但对于小数目的侦听器非常有效)。请参阅此模式的示例。旁白:此处的
CopyOnWriteArrayList
可能会更好。我认为这一切都是为了减轻一种病态情况,即其中一个侦听器在其onProgress
实现过程中调用addListener
或RemovelListener
。@OliverCharlesworth,但肯定如此这与死锁无关,而是会导致ConcurrentModificationException
@AndyTurner-我认为原始代码会死锁。通过输入updateProgress
,然后调用listener.onProgress
,然后调用addListener
,尝试获取锁。噢,除了同步的方法是可重入的之外。所以我不明白这里到底会出现什么问题。甚至比我想象的还要病态;)@OliverCharlesworth我认为唯一合适的回答是“muhahahaha”。或者使用多播模式,这在听众人数较少时尤其有效。
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();
}
}