Java并发中的外星方法很难理解

时间:2019-06-10 15:31:32

标签: java concurrency jvm

我正在阅读《七周内的七个并发模型》,在第二章中有关于外来方法的描述。

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方法如何导致死锁,以及为什么克隆侦听器列表可以避免此问题。

2 个答案:

答案 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);。这样就可以释放它自己的锁。