我正在阅读《七周内的七个并发模型》,在第二章中有关于外来方法的描述。
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)
{
}
}
}
因为addListener(),removeListener()和updateProgress()都是 同步,多个线程可以调用它们而无需踩到一个 另一个脚趾。但是此代码中存在陷阱,可能导致 死锁,即使仅使用一个锁。问题是 该updateProgress()调用外来方法-它什么都不知道的方法 关于。该方法可以做任何事情,包括获取另一个 锁。如果是这样,那么我们已经不知不觉中获得了两个锁 是否按照正确的顺序进行。正如我们刚刚看到的, 导致死锁。唯一的解决方案是避免调用外来方法 按住锁。实现这一目标的一种方法是进行防御 在遍历之前复制侦听器的副本:
private void updateProgress(int n) {
ArrayList<ProgressListener> listenersCopy;
synchronized(this) {
listenersCopy = (ArrayList<ProgressListener>)listeners.clone();
}
for (ProgressListener listener: listenersCopy)
listener.onProgress(n);
}
在阅读了此说明之后,我仍然不明白onProgress
方法如何导致死锁,以及为什么克隆侦听器列表可以避免此问题。
答案 0 :(得分:2)
我仍然不明白onProgress方法如何导致死锁
假设我们有一个ProgressListener
实现
Downloader downloader = new Downloader();
downloader.addListener(new ProgressListener(){
public void onProgress(int n) {
// do something
Thread th = new Thread(() -> downloader.addListener(() -> {});
th.start();
th.join();
}
});
downloader.updateProgress(10);
对addListener
的第一次调用将成功。但是,当您调用updateProgress
时,将触发onProgress
方法。触发onProgress
时,它将永远不会完成,因为addListener
仍在获取锁的同时,正在调用onProgress
方法(在同步方法上阻塞)。这会导致死锁。
现在,这个示例很愚蠢,因为实际上并不会尝试创建死锁,但是复杂的代码路径和共享逻辑很容易导致某种形式的死锁。这里的重点是永远不要把自己放在那个位置
为什么克隆侦听器列表可以避免此问题。
您要对集合的访问进行同步,因为它被多个线程共享和变异。通过创建克隆,您不再允许其他线程更改您的集合(线程本地克隆)并且可以安全地遍历。
您仍会同步读取到克隆,但是onProgress
的执行发生在synchronization
之外。当您这样做时,我列出的示例将永远不会死锁,因为只有一个线程将获取Downloaded
监视器。
答案 1 :(得分:1)
重点是您永远不知道该方法的作用-例如,它可以尝试获取同一实例的锁。如果您一直持有该锁,它将饿死。
listener.onProgress(n);
可能还需要花费很多时间才能执行-如果您一直按住该锁,则addListener()
和removeListener()
会在这段时间内被阻止。尝试尽快释放锁。
复制列表的优点是您可以在释放锁定后调用listener.onProgress(n);
。这样就可以释放它自己的锁。