← Назад к вопросам

Какие проблемы могут возникнуть, если один поток инкрементит 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

Сравнение решений

ПодходAtomicityVisibilityPerformanceПростота
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 переменные без синхронизации

Вывод

Проблемы без синхронизации:

  1. ❌ count++ не атомарна — потеря обновлений
  2. ❌ Нет visibility — старые значения в кэше
  3. ❌ Instruction reordering — неправильный порядок операций
  4. ❌ Race conditions — непредсказуемые результаты
  5. ❌ Недетерминированное поведение — каждый запуск дает разный результат

Всегда используй синхронизацию для разделяемого состояния!