Сделать код накопления и обработки задачи потокобезопасным

68. Сделать обработку задач потокобезопасной

Условие задачи:
Задача сохраняется в общем поле, а затем обрабатывается отдельным методом.

Необходимо сделать составную операцию потокобезопасной и исключить ситуацию, при которой один поток обработает задачу другого.

Код:

private Task task;

public synchronized void accumulateTask(Task task) {
    this.task = task;
}

public synchronized void processTask() {
    // обработка task
}

public void doWork(Task task) {
    accumulateTask(task);
    processTask();
}

Спойлеры к решению

Подсказки
💡 Синхронизация отдельных методов не делает атомарной последовательность их вызовов.
💡 Между accumulateTask() и processTask() может выполниться другой поток.
💡 Можно синхронизировать весь метод doWork().
💡 Надёжнее отказаться от общего изменяемого поля и передавать задачу напрямую.

Решение

Минимальный вариант — синхронизировать всю составную операцию:

private Task task;

private void accumulateTask(Task task) {
    this.task = task;
}

private void processTask() {
    // обработка this.task
}

public synchronized void doWork(Task task) {
    accumulateTask(task);
    processTask();
}

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

Предпочтительный вариант — убрать общее изменяемое состояние:

public void doWork(Task task) {
    processTask(task);
}

private void processTask(Task task) {
    // обработка переданной задачи
}

Теперь каждый вызов работает со своим аргументом и не зависит от общего поля.

Такой вариант не требует synchronized, если processTask() не изменяет другое общее состояние и используемые зависимости также потокобезопасны.