这是原始代码。此程序可能最终导致死锁,因为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
中获取的原始锁定的时间。我理解为什么它会减少锁定的时间,但不能避免它如何避免使用锁定来调用异类方法。这是我的思路。
它创建了一个arraylist listeners
的克隆。此克隆是一个单独的对象,其中包含原始listener
具有的确切元素。
这是线程安全的,因为现在你有一个&#34; local&#34;复制,至少是该特定线程的本地,以及另一个线程对其本地副本的影响不会对您产生影响。
您可以通过onProgress
方法更新侦听器。但是,此更改仅适用于您的listeners
副本。
updateProgress
会返回,但&#34; local&#34;变化传播到&#34;原始&#34; listeners
?既然它是一个克隆版,它们会分开对象,但是它们如何将它们的更新相互通信?
那是我坚持的部分。
答案 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
方法的本地副本,而且任何其他代码都看不到。
因此,当通过addListener
或removeListener
对侦听器列表进行修改时,无论执行修改的线程是什么,它都会直接影响listener
字段引用的列表。你的对象,它仍然和以前一样。但是这种更改不会影响本地副本,updateProgress
方法正在迭代。由于updateProgress
仅迭代本地副本,因此它永远不会有任何需要传播到原始列表的修改。
请注意,在单线程场景中甚至需要此副本。许多List
实现,尤其是ArrayList
,不支持在有人迭代它时进行修改,即使修改是由执行迭代的同一线程进行的(除了通过修改列表之外)用于迭代的Iterator
相同,这里不适用。)
备选方案是CopyOnWriteArrayList
,它在迭代时不需要复制,但是当列表被修改时,或者在删除侦听器时可能只需要(部分)复制的Multicaster模式,但是当它可能变得低效时侦听器的数量变大(但对于小数字,非常有效)。有关此模式的示例AWTEventMulticaster
,请参阅implementation。