Как решить проблему с synchronized методом в многопоточности Java?

Для достоверности буду оперировать цитатами из книги Брюса Эккеля - Философия Java (4-е издание).

Логика нашей задачи будет простая:
Имеется класс, в котором есть переменная i, которую будет увеличивать и метод, который, в свою очередь, и будет заниматься увеличением значения переменной i.

Создаем макет класса:

public class AtomicityTest {
    private int i = 0;

    public int getValue() {
        return this.i;
    }

    public void evenIncrement() {
        i++;
        i++;
    }
}

У нас есть переменная i, к которой в дальнейшем будут обращаться несколько потоков. По этому поводу Брюс Эккель пишет:

Если сразу несколько задач обращаются к полю, это поле следует объявить с ключевым словом volatile.

Хорошо, теперь поле нашего класса должно выглядеть следующим образом:

private volatile int i = 0;

Но ниже он пишет:

в виртуальной Java машине инкремент не является атомарным...

В примере в методе мы будем использовать атомарную операцию инкрементирования ++. Атомарная операция - это:

операция, которую не может прервать планировщик потоков — если она начинается, то продолжается до завершения, без возможности переключения контекста.

Что мы в итоге имеем? Получается, что операции ++ не является атомарной, следовательно, нету смысла объявлять её volatile, верно?

Читаем дальше

Для управления доступом к совместному ресурсу вы для начала помещаете его внутрь объекта. После этого любой метод, получающий доступ к ресурсу, может быть объявлен как synchronized. Это означает, что если задача выполняется внутри одного из объявленных как synchronized методов, все остальные задачи не сумеют зайти в свои synchronized-методы до тех пор, пока первая задача не вернет управление из своего вызова.

На выходе имеем готовый класс:

public class AtomicityTest implements Runnable {
    private int i = 0;

    public int getValue() {
        return this.i;
    }

    public synchronized void evenIncrement() {
        i++;
        i++;
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }
}

Так как первоначальное значение переменной i равняется 0, а метод evenIncrement() прибавляет по единице к переменной, в итоге получаем только четные числа. Так как метод синхронизированный, то:

  • поток № 1 зайдет в метод;
  • заблокирует доступ в метод для других потоков;
  • сделает так, чтобы переменная увеличилась на 2 (0, 2, 4, 6, 8 и т.д.)
  • поток № 1 выйдет и разблокирует доступ к методу для других потоков.

Хорошо, теперь приступаем к тестированию:

AtomicityTest at = new AtomicityTest();

        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(at);

        while (true) {
            int value = at.getValue();

            if(value % 2 != 0) {
                System.out.println("value: " + value);
                System.exit(0);
            }
        }

И тут чудеса. Получается так, что при тестировании мы на выходе получаем нечетные числа. Как такое возможно? Решение проблемы кроется в том, что метод, возвращающий значение i с именем getValue() должен быть объявлен таким образом:

public synchronized int getValue() {
    ...
}

Но почему только так? Всё ведь подтверждено цитатами выше и должно работать, по логике, без синхронизированного метода getValue().


Ответы (1 шт):

Автор решения: Regent

Про volatile подробнее расписано, например, в этом вопросе. Если вкратце, то volatile это в первую очередь про видимость значения между потоками, а не про атомарность

В цитате

все остальные задачи не сумеют зайти в свои synchronized-методы до тех пор, пока первая задача не вернет управление из своего вызова

говорится о том, что если вызван synchronized метод, то нельзя вызвать из другого потока другой synchronized метод

В документации формулировка аналогичная:

When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution)

Всё это никак не противоречит тому, что можно параллельно вызвать не-synchronized метод, что и происходит на практике

→ Ссылка