54. Разобраться с потоками и AtomicInteger
Условие задачи:
Дан код с несколькими потоками. Необходимо:
определить, что может быть выведено в консоль;
объяснить проблему текущей реализации;
реализовать корректный счётчик с помощью
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);
}
}
Здесь увеличение и вывод выполняются внутри одной критической секции.