简体   繁体   中英

Alien method in java concurrency is hard to understand

I'm reading book "Seven Concurrency Models in Seven Weeks", and there is a description about alien method in chapter 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)
        {
        }
    }
}

Because addListener(), removeListener(), and updateProgress() are all synchronized, multiple threads can call them without stepping on one another's toes. But a trap lurks in this code that could lead to deadlock even though there's only a single lock in use. The problem is that updateProgress() calls an alien method—a method it knows nothing about. That method could do anything, including acquiring another lock. If it does, then we've acquired two locks without knowing whether we've done so in the right order. As we've just seen, that can lead to deadlock. The only solution is to avoid calling alien methods while holding a lock. One way to achieve this is to make a defensive copy of listeners before iterating through it:

private void updateProgress(int n) { 
    ArrayList<ProgressListener> listenersCopy; 
    synchronized(this) {
        listenersCopy = (ArrayList<ProgressListener>)listeners.clone();
    }
    for (ProgressListener listener: listenersCopy)
        listener.onProgress(n);
}

After I read this explanation, I still can't understand how the onProgress method can cause dead lock, and why clone the listener list can avoid the problem.

I still can't understand how the onProgress method can cause dead lock

Let's imagine we have a ProgressListener implementation of

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);

The first call to addListener will succeed. When you invoke updateProgress though, the onProgress method will be triggered. When the onProgress is triggered it will never complete as the addListener method is being called (blocking on the sync method) while the onProgress is still acquiring the lock. This results in a deadlock.

Now, this example is a silly one as one wouldn't actually try to create a deadlock, but complex code paths and shared logic can easily result in some form of a deadlock. The point here is to never put yourself in that position

why clone the listener list can avoid the problem.

You want to synchronize on access of the collection because it is shared and mutated by multiple threads. By creating a clone, you are no longer allowing other threads to mutate your collection (thread local clone) and are safe to traverse.

You still synchronize on read to clone, but the execution of the onProgress is occurring outside of synchronization . When you do that, my example I have listed will never deadlock as only one thread will be acquiring the Downloaded monitor.

The point is that you never know what that method does - it could, for example, try to acquire a lock on the same instance. If you keep holding the lock, it will die of starvation.

Also listener.onProgress(n); might take a lot of time to execute - if you keep holding the lock, addListener() and removeListener() are blocked for that time. Try to release locks as soon as possible.

The advantage of copying the list is that you can call listener.onProgress(n); after releasing the lock. So it can acquite it's own lock.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM