1

Для достоверности буду оперировать цитатами из книги Брюса Эккеля - Философия 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().

West Side
  • 578
  • Но почему только так? Всё ведь подтверждено цитатами выше и должно работать, по логике, без синхронизированного метода getValue(). с чего бы это? Откуда такие выводы? – tym32167 May 31 '21 at 13:34
  • Про volatile есть вопрос, а насчет synchronized getValue - в цитате "все остальные задачи не сумеют зайти в свои synchronized-методы до тех пор, пока первая задача не вернет управление из своего вызова" ведь не говорится, что "не сумеют зайти в любой метод" - не synchronized методы могут быть вызваны когда угодно – Regent May 31 '21 at 13:34
  • @Regent, точно. Как правильно теперь дать ответ на вопрос? Отвечайте, будем принимать... – West Side May 31 '21 at 14:06
  • Попробую растянуть комментарий на ответ – Regent May 31 '21 at 14:27
  • Синхронизованные методы очень похожи на синхронизованные блоки, если используется synchronized(this) в теле метода. См. здесь как надо использовать ключевое слово synchronized. – Roman C May 31 '21 at 14:28
  • чтобы получать только четные числа нужно чтобы и метод getValue() был synchronized, так работает механизм, а то можно получить значение поля между двумя инкрементами. А так чтение поля тоже будет ждать своей очереди. – Ansar Ozden May 31 '21 at 14:30
  • @Ansar Если сделать "чтобы и метод getValue() был synchronized", то это ударит по производительности, а также может привести к дедлокам. – Roman C May 31 '21 at 14:55
  • Ну тогда внутри evenIncrement() должна быть только одна модификация счётчика, например добавлять сразу 2. А лучше использовать атомик классы, у них, говорят, оптимизация низкоуровневая. – Ansar Ozden May 31 '21 at 15:03

1 Answers1

1

Про 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 метод, что и происходит на практике

Regent
  • 19,134
  • В документации написано верно, но не следует цитировать английский текст. – Roman C May 31 '21 at 14:41
  • @RomanC, а я, наоборот, приветствую первоисточник. Каждый сам для себя переведен. – West Side May 31 '21 at 15:41