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

Какая проблема возникнет при десяти потоках, выполняющих synchronized?

2.3 Middle🔥 171 комментариев
#Многопоточность

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

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

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

Проблемы при десяти потоках, выполняющих synchronized блок

Когда несколько потоков конкурируют за один synchronized блок, возникает ряд проблем с производительностью и поведением. Это критическое знание для многопоточного программирования.

1. Монопольный доступ (Lock Contention)

Текущее состояние:
Thread 1: ████ (входит в synchronized, получает блокировку)
Thread 2: ░░░░ (ждёт)
Thread 3: ░░░░ (ждёт)
Thread 4: ░░░░ (ждёт)
...
Thread 10: ░░░░ (ждёт)

Одновременно выполнять может только ОДИН поток!
Остальные ждут в очереди на монитор объекта

2. Пример проблемы

public class Counter {
  private int count = 0;
  
  public synchronized void increment() {
    count++; // 1 наносекунда
  }
  
  public int getCount() {
    return count;
  }
}

// Создаём 10 потоков, каждый вызывает increment() 100000 раз
Counter counter = new Counter();
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10; i++) {
  executor.submit(() -> {
    for (int j = 0; j < 100000; j++) {
      counter.increment(); // Все конкурируют за один монитор
    }
  });
}

// Результат: вместо параллелизма — 90% времени тратится на ожидание блокировки

3. Проблема 1: Снижение производительности (Performance Degradation)

Без потоков:        1 поток выполняет 1M операций
Время: 100ms

С 10 потоками synchronized:
- Каждый поток выполняет в среднем 1/10 работы = 100K операций
- Но вместо параллелизма происходит очередность
- Один поток исполняется, 9 ждут
- Реальное время: 1000ms (10x медленнее!)

Параллелизм ОТСУТСТВУЕТ

4. Проблема 2: Context Switching (переключение контекста)

// ОС переключает потоки тысячи раз в секунду
// Каждое переключение = потеря кэша CPU

Thread t1: работает 0.1ms → сохраняет состояние
Thread t2: работает 0.1ms → сохраняет состояние
...
Thread t10: работает 0.1ms → сохраняет состояние
(вернулся к t1)

// Кэш процессора потерялся!
// L1 cache: 32-64 KB per core
// Context switch: ~1000 циклов процессора

5. Проблема 3: Priority Inversion (инверсия приоритетов)

public synchronized void criticalMethod() {
  // ПОЛНАЯ БЛОКИРОВКА на время выполнения
  // Даже high-priority потоки ждут!
}

// Сценарий
Thread 1 (приоритет 10): выполняет synchronized
Thread 2 (приоритет 1):  ЖДЁТ БЛОКИРОВКИ
Thread 3 (приоритет 10): ЖДЁТ БЛОКИРОВКИ

// Потоки с высоким приоритетом заблокированы низкоприоритетным потоком

6. Проблема 4: Deadlock (взаимная блокировка)

// Объект A и B, каждый со своей блокировкой
Object objA = new Object();
Object objB = new Object();

// Thread 1
synchronized (objA) {
  System.out.println("T1: locked A");
  Thread.sleep(100);
  synchronized (objB) {  // Ждёт B!
    System.out.println("T1: locked B");
  }
}

// Thread 2
synchronized (objB) {
  System.out.println("T2: locked B");
  Thread.sleep(100);
  synchronized (objA) {  // Ждёт A!
    System.out.println("T2: locked A");
  }
}

// DEADLOCK! Оба потока ждут друг друга
// Программа зависает навсегда

7. Проблема 5: Starvation (голодание потока)

public synchronized void method() {
  // Если это долгая операция, остальные потоки могут вообще не получить доступ
  for (int i = 0; i < 1000000; i++) {
    doHeavyWork(); // 10 секунд
  }
}

// Результат
// Thread 1: 0-10s работает
// Thread 2: 10-20s работает
// ...
// Thread 10: 90-100s ждал, потом работает
// = 50+ секунд среднего ожидания

8. Мониторинг проблемы

public class SyncProblemDemo {
  private int counter = 0;
  
  public synchronized void slowIncrement() {
    counter++;
    try {
      Thread.sleep(1); // Эмулируем долгую работу
    } catch (InterruptedException e) {}
  }
  
  public static void main(String[] args) throws InterruptedException {
    SyncProblemDemo demo = new SyncProblemDemo();
    long start = System.currentTimeMillis();
    
    ExecutorService executor = Executors.newFixedThreadPool(10);
    
    for (int i = 0; i < 10; i++) {
      executor.submit(() -> {
        for (int j = 0; j < 100; j++) {
          demo.slowIncrement();
        }
      });
    }
    
    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
    
    long duration = System.currentTimeMillis() - start;
    System.out.println("Total time: " + duration + "ms");
    // Ожидаемо: ~1000ms (10 потоков * 100 операций * 1ms)
    // Реально: ~1100ms (плюс overhead синхронизации)
    // БЕЗ синхронизации было бы: ~100ms (параллелизм)
  }
}

9. Решение 1: Lock-free структуры данных

// Вместо synchronized — AtomicInteger
AtomicInteger counter = new AtomicInteger(0);

ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10; i++) {
  executor.submit(() -> {
    for (int j = 0; j < 100000; j++) {
      counter.incrementAndGet(); // Compare-and-swap без блокировки
    }
  });
}

// Результат: 10x быстрее (реальный параллелизм)

10. Решение 2: ReentrantLock с гранулярностью

public class OptimizedCounter {
  private static final int BUCKETS = 10;
  private final int[] counts = new int[BUCKETS];
  private final Lock[] locks = new ReentrantLock[BUCKETS];
  
  public OptimizedCounter() {
    for (int i = 0; i < BUCKETS; i++) {
      locks[i] = new ReentrantLock();
    }
  }
  
  public void increment(int id) {
    int bucket = id % BUCKETS;  // Распредели нагрузку на 10 бакетов
    locks[bucket].lock();
    try {
      counts[bucket]++;
    } finally {
      locks[bucket].unlock();
    }
  }
}

// Теперь 10 потоков конкурируют за 10 разных локов
// Реальный параллелизм!

11. Решение 3: StampedLock (Java 8+)

public class OptimizedData {
  private StampedLock lock = new StampedLock();
  private int value = 0;
  
  // Оптимистичное чтение (без блокировки!)
  public int read() {
    long stamp = lock.tryOptimisticRead();
    int result = value;
    if (!lock.validate(stamp)) {
      // Перечитаем если были изменения
      stamp = lock.readLock();
      try {
        result = value;
      } finally {
        lock.unlockRead(stamp);
      }
    }
    return result;
  }
  
  // Запись с полной блокировкой
  public void write(int v) {
    long stamp = lock.writeLock();
    try {
      value = v;
    } finally {
      lock.unlockWrite(stamp);
    }
  }
}

12. Таблица сравнения подходов

┌────────────────────┬──────────────┬────────────────┬──────────┐
│ Подход             │ Тип         │ Производ.     │ Сложност │
├────────────────────┼──────────────┼────────────────┼──────────┤
│ synchronized       │ Pessimistic  │ Низкая 10x+   │ Низкая   │
│ AtomicInteger      │ Lock-free    │ Хорошая 2x    │ Средняя  │
│ ReentrantLock      │ Explicit     │ Хорошая 5x    │ Высокая  │
│ StampedLock        │ Optimistic   │ Отличная 1x   │ Высокая  │
│ immutable objects  │ Keine Locks  │ Отличная 1x   │ Средняя  │
└────────────────────┴──────────────┴────────────────┴──────────┘

Итоги: проблемы synchronized с 10 потоками

  • Lock Contention — все ждут одного монитора
  • Performance Degradation — параллелизм становится последовательностью
  • Context Switching — потеря кэша процессора
  • Deadlock Риск — взаимные блокировки
  • Starvation — некоторые потоки могут не получить доступ
  • Priority Inversion — низкоприоритетный поток блокирует высокоприоритетный

Рекомендация:

  • Для простых операций: AtomicInteger
  • Для сложных логик: ReentrantLock или StampedLock
  • Для избежания проблем: immutable objects или message passing