Инопланетный метод в java-параллелизме трудно понять - PullRequest
1 голос
/ 10 июня 2019

Я читаю книгу "Семь моделей параллелизма за семь недель", и в главе 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 может вызватьмертвая блокировка, и почему клонирование списка слушателей может избежать проблемы.

Ответы [ 2 ]

2 голосов
/ 10 июня 2019

Я до сих пор не могу понять, как метод 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 голос
/ 10 июня 2019

Дело в том, что вы никогда не знаете, что делает этот метод - он может, например, попытаться получить блокировку для того же экземпляра.Если вы продолжите удерживать блокировку, она умрет от голода.

Кроме того, listener.onProgress(n); может занять много времени для выполнения - если вы продолжите удерживать блокировку, addListener() и removeListener() заблокированы для этоговремя.Попробуйте снять блокировки как можно скорее.

Преимущество копирования списка состоит в том, что вы можете позвонить listener.onProgress(n); после снятия блокировки.Так что он может оправдать свой собственный замок.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...