Потоки, синхронизация и реализация AtomicInteger

54. Разобраться с потоками и AtomicInteger

Условие задачи:
Дан код с несколькими потоками. Необходимо:

  1. определить, что может быть выведено в консоль;

  2. объяснить проблему текущей реализации;

  3. реализовать корректный счётчик с помощью AtomicInteger.

Код:

public class Question9 {

    public static class TestClass extends Thread {

        private static int threadCounter = 0;

        @Override
        public void run() {
            ++threadCounter;
            System.out.println(threadCounter);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            synchronized (TestClass.class) {
                new TestClass().start();
            }
        }
    }
}

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

Подсказки
💡 Блок synchronized защищает только вызов start(), но не выполнение run().
💡 Операция ++threadCounter состоит из чтения, увеличения и записи.
💡 Несколько потоков могут одновременно прочитать одно значение.
💡 volatile обеспечит видимость, но не сделает инкремент атомарным.
💡 Для счётчика можно использовать AtomicInteger.incrementAndGet().

Решение

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

Возможен корректный вывод:

1
2
3
4
5
6
7
8
9
10

Но из-за гонки данных возможны повторяющиеся значения, пропуски и другой порядок:

2
1
3
4
4
5
6
7
8
9

Проблема находится в операции:

++threadCounter;

Она не является атомарной. Несколько потоков могут прочитать одинаковое значение счётчика и записать одинаковый результат.

Синхронизация в main() не решает проблему:

synchronized (TestClass.class) {
    new TestClass().start();
}

Метод start() запускает поток асинхронно и завершается. Сам метод run() выполняется позже и не захватывает монитор TestClass.class.

Корректная реализация через AtomicInteger:

public class Question9 {

    public static class TestClass extends Thread {

        private static final AtomicInteger THREAD_COUNTER =
                new AtomicInteger();

        @Override
        public void run() {
            int value = THREAD_COUNTER.incrementAndGet();
            System.out.println(value);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new TestClass().start();
        }
    }
}

incrementAndGet() атомарно увеличивает счётчик и возвращает новое значение. Поэтому каждое число от 1 до 10 будет получено ровно одним потоком, хотя порядок вывода останется произвольным.

Альтернативный вариант через синхронизацию:

@Override
public void run() {
    synchronized (TestClass.class) {
        threadCounter++;
        System.out.println(threadCounter);
    }
}

Здесь увеличение и вывод выполняются внутри одной критической секции.