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

Какие принципы работы sinchronized

2.0 Middle🔥 181 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы при многопоточном доступе к static int

Введение

Это один из самых коварных вопросов в Java, потому что код выглядит просто, но скрывает серьёзные проблемы. Давайте разберём все слои сложности.

Проблема 1: Race Condition (Условие гонки)

Операция count++ в Java на самом деле состоит из трёх шагов:

private static int count = 0;

// Thread 1: count++  это на самом деле:
// 1. READ: int temp = count;        // Прочитать значение
// 2. ADD:  temp = temp + 1;         // Инкрементировать
// 3. WRITE: count = temp;           // Записать обратно

// Если Thread 2 читает в промежутке, может быть проблема!

Сценарий гонки:

public class RaceConditionExample {
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        // Thread 1: инкрементит
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count++;  // 3 операции
            }
        });
        
        // Thread 2: читает
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("Count: " + count);  // Может быть между increment и write
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Final count: " + count);  // Может быть не 100!
    }
}

// Возможный вывод:
// Count: 0
// Count: 1
// Count: 1  // Видит старое значение!
// Count: 3
// Final count: 58  // Потеряны обновления!

Проблема 2: Видимость данных (Visibility)

Ожидание: когда Thread 1 пишет значение, Thread 2 должен увидеть новое значение. На самом деле это не гарантировано без синхронизации!

public class VisibilityProblem {
    private static int counter = 0;
    private static boolean flag = false;
    
    public static void main(String[] args) {
        // Thread 1: пишет
        new Thread(() -> {
            counter = 42;
            flag = true;  // Сигнал, что counter готов
        }).start();
        
        // Thread 2: ждёт signal и читает counter
        new Thread(() -> {
            while (!flag) {  // Может крутиться вечно!
                // flag изменилась в памяти, но Thread 2 видит кэшированное значение
            }
            System.out.println("Counter: " + counter);
            // Может напечатать 0 вместо 42!
        }).start();
    }
}

Почему? CPU кэширует значения переменных в L1/L2 кэше. Изменение одного ядра не видно другому ядру сразу!

Проблема 3: Переупорядочение инструкций (Instruction Reordering)

Компилятор или CPU может переупорядочить инструкции для оптимизации, нарушив логику.

public class ReorderingProblem {
    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;
    
    public static void main(String[] args) {
        // Thread 1
        new Thread(() -> {
            a = 1;      // Инструкция 1
            x = b;      // Инструкция 2 (зависит ли от 1? Нет!)
            // Может выполниться как:
            // Инструкция 2
            // Инструкция 1
        }).start();
        
        // Thread 2
        new Thread(() -> {
            b = 1;      // Инструкция 3
            y = a;      // Инструкция 4
        }).start();
        
        // Возможны ситуации, когда x = 0 И y = 0 (оба увидели старые значения)
        // Это невозможно без переупорядочения!
    }
}

Проблема 4: Lost Updates (Потеря обновлений)

Если два потока одновременно инкрементят count++, некоторые инкременты теряются.

public class LostUpdate {
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 10000; i++) {
                count++;  // Потенциальные потери!
            }
        };
        
        Thread t1 = new Thread(increment);
        Thread t2 = new Thread(increment);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Expected: 20000, Actual: " + count);
        // Вывод: Expected: 20000, Actual: 13487
        // 6513 обновлений потеряны!
    }
}

Проблема 5: Несогласованное состояние при составных операциях

Читая между write'ом и read'ом, Thread 2 может увидеть промежуточное состояние.

public class InconsistentState {
    private static int value1 = 0;
    private static int value2 = 0;
    
    public static void main(String[] args) {
        // Thread 1: обновляет оба значения
        new Thread(() -> {
            while (true) {
                value1 = 1;
                value2 = 1;  // Когда это прочитает Thread 2?
            }
        }).start();
        
        // Thread 2: хочет видеть согласованное состояние
        new Thread(() -> {
            while (true) {
                int v1 = value1;
                int v2 = value2;
                if ((v1 == 0 && v2 == 1) || (v1 == 1 && v2 == 0)) {
                    System.out.println("INCONSISTENT STATE: v1=" + v1 + ", v2=" + v2);
                    // Это МОЖЕТ произойти!
                }
            }
        }).start();
    }
}

Проблема 6: Инициализация final переменных

Если static int используется вместе с другими переменными, проблема может быть и с инициализацией.

public class UnsafePublication {
    private static int id;          // Не final!
    private static String name;     // Не final!
    
    public static void main(String[] args) {
        // Thread 1: инициализирует
        new Thread(() -> {
            id = 42;
            name = "User";
            // Кто-то может видеть id=42 и name=null (old value)!
        }).start();
        
        // Thread 2: читает
        new Thread(() -> {
            System.out.println("ID: " + id + ", Name: " + name);
            // Может напечатать: ID: 42, Name: null
        }).start();
    }
}

Реальный пример из практики

public class EventCounter {
    public static int eventCount = 0;
    
    public static void handleEvent(String event) {
        eventCount++;  // ОПАСНО!
        // Миллионы событий в день, потеря обновлений = потеря данных
    }
}

Решения

1. Использовать volatile (простое решение, но не полное)

private static volatile int count = 0;

// volatile гарантирует:
// - Видимость: изменение видно другим потокам
// - Порядок выполнения: нет переупорядочения
// - НО: не гарантирует атомарность count++!

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            count++;  // Всё ещё race condition!
        }
    });
    
    Thread t2 = new Thread(() -> {
        System.out.println(count);  // Видит актуальное значение
    });
    
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    
    System.out.println(count);  // Может быть не 100!
}

2. Синхронизировать доступ (полное решение для incement'а)

private static int count = 0;

public synchronized static void increment() {
    count++;  // Теперь атомарно
}

public synchronized static int getCount() {
    return count;
}

// Или с реентрантным lock'ом
private static final ReentrantLock lock = new ReentrantLock();
private static int count = 0;

public static void increment() {
    lock.lock();
    try {
        count++;  // Атомарно
    } finally {
        lock.unlock();
    }
}

3. Использовать AtomicInteger (лучший вариант)

private static final AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            count.incrementAndGet();  // Атомарно, без lock'ов
        }
    });
    
    Thread t2 = new Thread(() -> {
        System.out.println(count.get());  // Видит актуальное значение
    });
    
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    
    System.out.println(count.get());  // Точно 100!
}

Таблица решений

РешениеАтомарностьВидимостьПроизводительностьИспользование
intБыстроОпасно!
volatile intНормальноТолько чтение/запись
synchronizedМедленноСложные операции
AtomicIntegerБыстро++, get, set
ReentrantLockНормальноГибкость

Заключение

Этот вопрос выявляет понимание многопоточности. Ответ должен включать:

  1. Race condition — несколько операций могут перекрыться
  2. Visibility — кэширование в CPU может скрыть изменения
  3. Reordering — компилятор может переупорядочить инструкции
  4. Lost updates — некоторые инкременты могут быть потеряны
  5. Решение — AtomicInteger, synchronized или volatile

Профессиональный разработчик никогда не пишет unsynchronized доступ к shared mutable state.

Какие принципы работы sinchronized | PrepBro