【问题标题】:Alien method in java concurrency is hard to understandjava并发中的外星人方法很难理解
【发布时间】:2019-06-10 15:31:32
【问题描述】:

我正在看《七周的七个并发模型》一书,第2章有关于异形方法的描述。

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方法怎么会导致死锁,为什么克隆监听列表可以避免这个问题。

【问题讨论】:

  • 附带说明,您最好使用listenersCopy = new ArrayList&lt;&gt;(listeners);
  • @Holger 使用 ArrayList 构造函数有什么好处?我检查了源代码,似乎它们都在后台使用 Arrays.copyOf 方法。
  • 您不需要执行未经检查的类型转换。而clone() 通常是要避免的。

标签: java concurrency jvm


【解决方案1】:

我还是不明白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 监视器。

【讨论】:

    【解决方案2】:

    关键是您永远不知道该方法的作用——例如,它可能会尝试在同一个实例上获取锁。如果你一直握着锁,它会饿死的。

    另外,listener.onProgress(n); 可能需要很长时间才能执行 - 如果你一直持有锁,addListener()removeListener() 会在这段时间内被阻塞。尝试尽快释放锁。

    复制列表的好处是释放锁后可以调用listener.onProgress(n);。所以它可以释放它自己的锁。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-22
      • 1970-01-01
      • 1970-01-01
      • 2010-11-07
      • 1970-01-01
      • 2018-08-23
      相关资源
      最近更新 更多