← Назад к вопросам
Какие проблемы могут возникнуть, если один поток инкрементит static int, а другой только читает его
2.0 Middle🔥 171 комментариев
#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Race Condition: один поток инкрементит static int, другой читает его
Проблема
Когда несколько потоков одновременно обращаются к разделяемой переменной без синхронизации, возникает race condition — результат зависит от порядка выполнения потоков.
// Небезопасный код
public class Counter {
public static int count = 0;
public static void increment() {
count++; // ОПАСНО: три операции
}
public static int getCount() {
return count; // ОПАСНО: может прочитать промежуточное значение
}
}
Проблема 1: count++ не атомарна
Операция count++ состоит из трёх шагов на уровне байт-кода:
count++; // Выглядит как одна операция
// На самом деле это:
// 1. GETSTATIC count (прочитать значение)
// 2. ICONST_1 (загрузить константу 1)
// 3. IADD (сложить)
// 4. PUTSTATIC count (записать результат)
Сценарий race condition:
Время | Поток 1 | Поток 2 | count
-----+------------------+-------------------+------
1 | GETSTATIC count | | 0
2 | | GETSTATIC count | 0
3 | IADD (0+1) | | 0
4 | | IADD (0+1) | 0
5 | PUTSTATIC count | | 1
6 | | PUTSTATIC count | 1 (ОШИБКА!)
Оба потока прочитали 0, оба добавили 1, оба написали 1. Результат: count=1 вместо 2.
// Демонстрация race condition
public class RaceConditionDemo {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int iteration = 0; iteration < 10; iteration++) {
count = 0;
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
count++; // Race condition
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
count++; // Race condition
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// Результат должен быть 200000, но это не гарантировано
System.out.println("Iteration " + iteration + ": count = " + count);
// Вывод может быть:
// Iteration 0: count = 189234 (не 200000!)
// Iteration 1: count = 198456 (не 200000!)
// Iteration 2: count = 200000 (случайно совпало)
}
}
}
Проблема 2: Memory Visibility (Видимость памяти)
Без синхронизации нет гарантии, что изменения видны другим потокам:
public class VisibilityProblem {
public static int count = 0;
public static boolean done = false;
public static void main(String[] args) throws InterruptedException {
// Поток 1: пишет
Thread writer = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
count++; // Пишет значение
}
done = true; // Сигнализирует о завершении
});
// Поток 2: читает
Thread reader = new Thread(() -> {
while (!done) {
int value = count; // Может не видеть обновления!
}
System.out.println("Final count: " + count);
});
writer.start();
reader.start();
writer.join();
reader.join();
// Reader может зависнуть в бесконечном цикле!
// Потому что done=true никогда не станет видимой для reader
}
}
Потребитель (Poller) может вращаться в цикле часами, не видя done = true.
Проблема 3: Instruction Reordering (Переупорядочение инструкций)
Компилятор и процессор могут переупорядочить инструкции:
public class ReorderingProblem {
public static int count = 0;
public static boolean ready = false;
public static void write() {
count = 42; // Инструкция 1
ready = true; // Инструкция 2
}
public static void read() {
if (ready) {
System.out.println(count); // Может быть 0 вместо 42!
}
}
}
// Проблема:
// 1. Компилятор переупорядочил:
// ready = true; // Инструкция 2 выполнилась первой!
// count = 42; // Инструкция 1 выполнилась второй
//
// 2. Reader видит ready=true
// 3. Reader читает count=0 (потому что count=42 ещё не выполнено)
// 4. Выведет 0 вместо 42
Проблема 4: Cached Values (Кэшированные значения)
Процессор может кэшировать значение в регистре:
public class CachingProblem {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
// Поток 1: читает в цикле
Thread reader = new Thread(() -> {
int localCount = 0;
while (localCount < 100) {
localCount = count; // Может быть кэшировано в регистре
System.out.println("Read: " + localCount);
}
});
// Поток 2: пишет
Thread writer = new Thread(() -> {
for (int i = 1; i <= 100; i++) {
count = i; // Пишет в memory
try {
Thread.sleep(1); // Даёт время reader читать
} catch (InterruptedException e) {}
}
});
reader.start();
writer.start();
reader.join();
writer.join();
}
}
// Проблема:
// Reader может кэшировать count в регистре и никогда не заметить обновления
// Даже если Writer пишет в memory
Проблема 5: Lost Updates (Потеря обновлений)
Частный случай race condition:
public class LostUpdateDemo {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 10 потоков, каждый инкрементит 10000 раз
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count++; // Race condition
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
// Ожидаем: 100000
// Получаем: ~85000-99000 (случайно)
System.out.println("Expected: 100000");
System.out.println("Actual: " + count);
}
}
Решение 1: Синхронизация (synchronized)
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++; // Защищено мютексом
}
public static synchronized int getCount() {
return count; // Защищено мютексом
}
}
// Гарантии:
// 1. count++ выполнится атомарно
// 2. Все потоки видят последнее значение
// 3. Нет переупорядочения инструкций
Решение 2: volatile (частичное решение)
public class Counter {
private static volatile int count = 0;
// volatile гарантирует visibility, но не atomicity!
public static void increment() {
count++; // ВСЁ ЕЩЕ НЕБЕЗОПАСНО!
// volatile видно всем потокам, но count++ не атомарна
}
public static int getCount() {
return count; // Безопасно: видно последнее значение
}
}
// volatile помогает с visibility, но не с atomicity
Решение 3: AtomicInteger (правильное)
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private static AtomicInteger count = new AtomicInteger(0);
public static void increment() {
count.incrementAndGet(); // Атомарная операция
}
public static int getCount() {
return count.get(); // Безопасное чтение
}
}
// AtomicInteger использует Compare-And-Swap (CAS) инструкцию
// Это быстрее чем synchronized
Решение 4: ReadWriteLock (для много читателей)
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Counter {
private static int count = 0;
private static ReadWriteLock lock = new ReentrantReadWriteLock();
public static void increment() {
lock.writeLock().lock();
try {
count++;
} finally {
lock.writeLock().unlock();
}
}
public static int getCount() {
lock.readLock().lock();
try {
return count; // Много потоков могут читать одновременно
} finally {
lock.readLock().unlock();
}
}
}
// Если читателей много, а писателей мало — это быстрее
Полная демонстрация проблемы
public class RaceConditionDemo {
// ❌ Небезопасно
public static class UnsafeCounter {
public static int count = 0;
public static void increment() { count++; }
public static int get() { return count; }
}
// ✅ Безопасно
public static class SafeCounter {
private static AtomicInteger count = new AtomicInteger(0);
public static void increment() { count.incrementAndGet(); }
public static int get() { return count.get(); }
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== UnsafeCounter ===");
UnsafeCounter.count = 0;
testCounter(() -> UnsafeCounter.increment(),
UnsafeCounter::get);
System.out.println("\n=== SafeCounter ===");
testCounter(() -> SafeCounter.increment(),
SafeCounter::get);
}
private static void testCounter(Runnable increment,
java.util.function.IntSupplier get)
throws InterruptedException {
for (int iteration = 0; iteration < 5; iteration++) {
// 2 потока, каждый инкрементит 100000 раз
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
increment.run();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
increment.run();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Iteration " + (iteration + 1) + ": " + get.getAsInt());
}
}
}
// Вывод:
// === UnsafeCounter ===
// Iteration 1: 123456 (не 200000!)
// Iteration 2: 189543 (не 200000!)
// Iteration 3: 200000 (случайно совпало)
// Iteration 4: 198765 (не 200000!)
// Iteration 5: 176234 (не 200000!)
//
// === SafeCounter ===
// Iteration 1: 200000
// Iteration 2: 200000
// Iteration 3: 200000
// Iteration 4: 200000
// Iteration 5: 200000
Сравнение решений
| Подход | Atomicity | Visibility | Performance | Простота |
|---|---|---|---|---|
| static int | ❌ | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| volatile | ❌ | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| synchronized | ✅ | ✅ | ⭐⭐ | ⭐⭐⭐ |
| AtomicInteger | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Lock | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Рекомендация
// 1. Для простых счётчиков — AtomicInteger
private static AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
// 2. Для критичного кода — synchronized
private static int count;
public synchronized void increment() {
count++;
}
// 3. Для много читателей — ReadWriteLock
private static ReadWriteLock lock = new ReentrantReadWriteLock();
// 4. НИКОГДА не используй разделяемые static переменные без синхронизации
Вывод
Проблемы без синхронизации:
- ❌ count++ не атомарна — потеря обновлений
- ❌ Нет visibility — старые значения в кэше
- ❌ Instruction reordering — неправильный порядок операций
- ❌ Race conditions — непредсказуемые результаты
- ❌ Недетерминированное поведение — каждый запуск дает разный результат
Всегда используй синхронизацию для разделяемого состояния!