Являются ли wait() и notify() ненадежными, несмотря на синхронизацию?

Недавно я обнаружил, что использование synchronized не предотвратит никаких мертвых блокировок.

Например. внутри этого кода:

ArrayList <Job> task;
...

public void do(Job job){
    synchronized(tasks){
        tasks.add(job);
    }
    synchronized(this){
        notify();
    }
}

public void run(){
    while(true){
        for (int = 0;i<tasks.size();i++){
            synchronized(tasks){
                Job job = tasks.get(i);
            }
            //do some job here...
        }
        synchronized(this){
            wait(); //lock will be lost...
            notifier = false; //lock will be acquired again after notify()
        }
    }
}

В чем проблема? Ну, а если работающий поток не ждет, то он не увидит никаких уведомлений (т.е. вызовов notify()), поэтому может попасть в мёртвую блокировку и не обработать полученные задачи! (Или он может справиться с ними слишком поздно...)

Поэтому я реализовал этот код:

private volatile boolean notifier = false;
ArrayList <Job> task;
...

public void do(Job job){
    synchronized(tasks){
        tasks.add(job);
    }
    synchronized(this){
        notifier = true;
        notify();
    }
}

public void run(){
    while(true){
        for (int = 0;i<tasks.size();i++){
            synchronized(tasks){
                Job job = tasks.get(i);
            }
            //do some job here...
        }
        synchronized(this){
            if(!notifier){
                wait(); //lock will be lost...
                notifier = false; //lock will be acquired again after notify()
            }
        }
    }
}

Это правильно или я что-то упускаю? А можно проще?


person Marcus    schedule 29.01.2014    source источник
comment
Писать потокобезопасный код сложно. Вы должны использовать существующую очередь блокировки (или просто исполнителя) вместо того, чтобы заново изобретать колесо.   -  person SLaks    schedule 29.01.2014
comment
Я склонен согласиться. Для подобных списков задач используйте какую-либо уже реализованную потокобезопасную очередь. Возможно, этот: docs.oracle.com/ javase/7/docs/api/java/util/concurrent/   -  person Robert Harvey    schedule 29.01.2014
comment
Хммм, изобретать велосипед скорее было бы созданием нового языка программирования. Так что нет, я не изобретаю велосипед, поскольку написание кода — это скорее проектирование автомобилей, чем изобретение колес. Но мне больше нравится Ferrari, а не Peugeot, а Ferrari — это скорость и простота. ;)   -  person Marcus    schedule 30.01.2014
comment
BlockingQueue - более эффективный и лучший шаблон @Marcus IMO. Вы должны хотя бы узнать о них.   -  person Gray    schedule 07.02.2014
comment
Хорошо, я решил принять ваш ответ (потому что он в конце концов правильный) и напишу несколько тестов, чтобы выяснить, какое решение наиболее эффективно. ;)   -  person Marcus    schedule 07.02.2014
comment
Спасибо. Я говорю это не только для того, чтобы набрать очки, Маркус. :-)   -  person Gray    schedule 07.02.2014


Ответы (3)


В чем проблема? Ну, а если работающий поток не ждет, то он не увидит никаких уведомлений (т.е. вызовов notify()), поэтому может попасть в мёртвую блокировку и не обработать полученные задачи!

Верно. Это не случай «ненадежности», а скорее случай определения языка. Вызов notify() не ставит уведомления в очередь. Если нет ожидающих потоков, то notify() фактически ничего не сделает.

можно ли сделать проще?

Да. Я бы рассмотрел возможность использования BlockingQueue -- LinkedBlockingQueue должен хорошо работать для вас. Один поток вызывает pull из очереди, а другой может добавлять в нее. Он позаботится о блокировке и сигнализации для вас. Вы должны быть в состоянии удалить большую часть вашего рукописного кода, как только вы начнете его использовать.

person Gray    schedule 29.01.2014
comment
+1 за признание того, что это проблема производителя/потребителя. - person Mike Samuel; 29.01.2014

Сначала я был обманут вашим вопросом. Ваша синхронизация (это) в объекте потока не имеет смысла. Раньше я также делал это, чтобы wait() не выдавало ошибку компиляции.

Только синхронизация (задачи) имеет смысл, поскольку вы ждете и хотите получить эти ресурсы.

Имея цикл for, это плохой дизайн. В проблеме потребитель-производитель. получить работу в то же время удалить работу. лучше получить работу один раз за раз.

public void do(Job job){
    synchronized(tasks){
        tasks.add(job);
        notify();
    }
}

public void run(){
    Job job;
    while(true){

        //This loop will fetch the task or wait for task notification and fetch again.
        while (true){
            synchronized(tasks){
                if(tasks.size()>0){
                    job = tasks.getTask(0);
                    break;
                }
                else
                    wait();

            }
        }

        //do some job here...

    }
}
person tom87416    schedule 29.01.2014
comment
Мне жаль, если вам не нравится этот комментарий, но wait(100) больше похоже на исправление кода, а не на реальный ответ. - person skiwi; 29.01.2014
comment
он может использовать семафоры или мониторы и для решения этой задачи. 1 шаг за раз. Понимать, как использовать ожидание и уведомление и его недостатки, и, следовательно, он будет искать другое лучшее решение. - person tom87416; 29.01.2014
comment
Можете ли вы подробно объяснить, почему тогда это должно быть 100 в wait(100)? - person skiwi; 29.01.2014
comment
Согласен, это должно быть ожидание (0) для неблокирующего потока или что-то вроде ожидания (5) или около того... но забавное решение, цикл while в цикле while. ;) - person Marcus; 30.01.2014
comment
Пересмотрено. это должно быть то, как вызвать ожидание и уведомить - person tom87416; 30.01.2014
comment
Просто для ясности: это должно было быть ожидание (1) или больше, так как ожидание (0) не выполнило бы эту работу... - person Marcus; 30.01.2014

На самом деле результатом является не тупиковая блокировка, а скорее зависание самой задачи/задания. Поскольку никакие потоки не «заблокированы», задача просто не будет выполнена, пока другой поток не вызовет do(Job job).

Ваш код почти правильный - помимо отсутствующей обработки исключений при вызове wait() и notify(). Но вы можете поместить task.size() в блок синхронизации и заблокировать задачи во время процесса дыры, потому что удаление задания внутри задач другим потоком приведет к сбою цикла:

...
while(true){
    synchronized(tasks){
        for (int = 0;i<tasks.size();i++){ //could be done without synchronisation
           Job job = tasks.get(i);        //if noone deletes any tasks
        }
        //do some job here...
    }
    ...

Просто обратите внимание, что ваш код блокирует. Неблокировка может быть быстрее и выглядеть так:

ArrayList <Job> tasks;
...

public void do(Job job){
    synchronized(tasks){
        tasks.add(job);
    }
}

public void run(){
    while(true){
        int length;
        synchronized(tasks){
            length = tasks.size();
        }
        for (int = 0;i<length;i++){
            Job job = tasks.get(i); //can be done without synchronisation if noone deletes any tasks...otherwise it must be within a synchronized block
            //do some job here...
        }
        wait(1); //wait is necessary and time can be set higher but never 0!
    }
}

Чему мы можем научиться? Ну, в неблокирующих потоках никакие notify(), wait() и synchronized не нужны. И установка ожидания (1) даже не использует больше ЦП в режиме ожидания (не устанавливайте ожидание (0), потому что это будет рассматриваться как ожидание ().

Однако будьте осторожны, поскольку использование wait(1) может быть медленнее, чем использование wait() и notify(): -efficient-than-using-wait-a">Является ли wait(1) в неблокирующем цикле while(true) более эффективным, чем использование wait() и notify()? (Другими словами: не -блокировка может быть медленнее, чем блокировка!)

person Marcus    schedule 29.01.2014
comment
Одно из лучших руководств по синхронизации можно найти здесь: javamex.com/tutorials/ - person Marcus; 30.01.2014