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

Как решить проблему голодания потоков?

1.0 Junior🔥 91 комментариев
#Другое

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

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

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

Как решить проблему голодания потоков?

Голодание потоков (Thread Starvation) — это состояние, когда один или несколько потоков не получают доступ к необходимым ресурсам (CPU, блокировкам) в течение длительного времени, потому что другие потоки занимают эти ресурсы.

Что такое голодание потоков?

Thread Starvation возникает, когда:

  • Поток с низким приоритетом никогда не получает CPU время
  • Поток ждёт блокировки, которую никогда не отпускают
  • Потоки в очереди ждут, пока завершатся другие задачи, но они не завершаются

Причина 1: Неправильный приоритет потоков

public class StarvationExample {
    public static void main(String[] args) {
        // Потребитель с HIGH приоритетом
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                System.out.println("Consumer: " + i);
            }
        });
        consumer.setPriority(Thread.MAX_PRIORITY); // Приоритет 10
        
        // Производитель с LOW приоритетом
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Producer: " + i);
            }
        });
        producer.setPriority(Thread.MIN_PRIORITY); // Приоритет 1
        
        consumer.start();
        producer.start();
        // Производитель может никогда не получить CPU!
    }
}

Решение: Используй одинаковые приоритеты для потоков или избегай установки приоритетов вообще.

// Правильно: оба потока имеют нормальный приоритет
Thread thread1 = new Thread(() -> { /* ... */ });
Thread thread2 = new Thread(() -> { /* ... */ });
// Не устанавливаем приоритет — используем NORM_PRIORITY по умолчанию
thread1.start();
thread2.start();

Причина 2: Захват блокировки в неправильном порядке

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    // Метод A: захватывает lock1, потом lock2
    public void methodA() {
        synchronized (lock1) {
            System.out.println("A захватил lock1");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock2) {
                System.out.println("A захватил lock2");
            }
        }
    }
    
    // Метод B: захватывает lock2, потом lock1 (ОБРАТНЫЙ ПОРЯДОК!)
    public void methodB() {
        synchronized (lock2) {
            System.out.println("B захватил lock2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock1) {
                System.out.println("B захватил lock1");
            }
        }
    }
    
    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();
        
        new Thread(example::methodA).start();
        new Thread(example::methodB).start();
        // DEADLOCK! Оба потока ждут друг друга
    }
}

Решение: Всегда захватывай блокировки в одинаковом порядке.

public class FixedVersion {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    // Метод A: lock1 → lock2
    public void methodA() {
        synchronized (lock1) {
            synchronized (lock2) {
                // Операции
            }
        }
    }
    
    // Метод B: ТАКЖЕ lock1 → lock2
    public void methodB() {
        synchronized (lock1) {
            synchronized (lock2) {
                // Операции
            }
        }
    }
}

Причина 3: ExecutorService с неограниченной очередью

// Плохо: может привести к голоданию если задачи завешивают
ExecutorService executor = Executors.newSingleThreadExecutor();

for (int i = 0; i < 1000000; i++) {
    executor.submit(() -> {
        // Долгая операция
        try { Thread.sleep(10000); } catch (InterruptedException e) {}
    });
}
// Очередь растёт неограниченно!

Решение: Используй bounded thread pool с ограниченной очередью.

import java.util.concurrent.*;

public class BoundedThreadPoolExample {
    public static void main(String[] args) throws Exception {
        // Пул с максимум 10 потоками и очередью на 100 задач
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            5,           // Core threads
            10,          // Max threads
            60,          // Keep alive time
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100) // ОГРАНИЧЕННАЯ очередь!
        );
        
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy() // Если очередь полна
        );
        
        // Теперь если очередь полна, выполнится в текущем потоке
        for (int i = 0; i < 200; i++) {
            executor.submit(() -> {
                System.out.println("Задача выполняется");
            });
        }
        
        executor.shutdown();
    }
}

Причина 4: Infinite loops в потоках

// Плохо: бесконечный цикл занимает CPU
public class BusyWaitingExample {
    private volatile boolean ready = false;
    
    public void waitForReady() {
        while (!ready) {
            // Busy waiting - пустой цикл! Другие потоки не получают CPU
        }
        System.out.println("Ready!");
    }
}

Решение: Используй proper synchronization primitives.

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ProperWaiting {
    private ReentrantLock lock = new ReentrantLock();
    private Condition readyCondition = lock.newCondition();
    private boolean ready = false;
    
    // Вместо busy waiting
    public void waitForReady() throws InterruptedException {
        lock.lock();
        try {
            while (!ready) {
                readyCondition.await(); // Поток спит, не занимает CPU
            }
            System.out.println("Ready!");
        } finally {
            lock.unlock();
        }
    }
    
    public void setReady() {
        lock.lock();
        try {
            ready = true;
            readyCondition.signalAll(); // Пробуждаем ждущие потоки
        } finally {
            lock.unlock();
        }
    }
}

Причина 5: Некорректное использование synchronized

// Плохо: синхронизируем слишком много кода
public class BadSynchronization {
    private List<String> list = new ArrayList<>();
    
    public synchronized void addAndProcess(String item) {
        list.add(item);
        
        // Долгая операция под блокировкой! Другие потоки ждут
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        
        String processed = item.toUpperCase();
        System.out.println(processed);
    }
}

Решение: Синхронизируй только критические части.

from java.util.concurrent.CopyOnWriteArrayList;

public class GoodSynchronization {
    private List<String> list = new CopyOnWriteArrayList<>();
    
    public void addAndProcess(String item) {
        // Критическая секция минимальна
        list.add(item);
        
        // Эта операция не требует синхронизации
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        String processed = item.toUpperCase();
        System.out.println(processed);
    }
}

Best Practices для избежания голодания

  1. Не используй приоритеты — держи все потоки на NORM_PRIORITY
  2. Захватывай блокировки в одном порядке — это предотвращает deadlock
  3. Используй non-blocking структуры данных из java.util.concurrent (ConcurrentHashMap, CopyOnWriteArrayList)
  4. Минимизируй время под блокировкой — синхронизируй только необходимое
  5. Используй ExecutorService правильно — с ограниченной очередью и бассейном потоков
  6. Избегай busy waiting — используй wait(), await(), или другие synchronization primitives
  7. Используй timeout'ы при ожидании — это предотвращает бесконечное ждание
// Пример с timeout
lock.tryLock(5, TimeUnit.SECONDS); // Ждём максимум 5 секунд
if (lock.tryLock()) {
    try {
        // критическая секция
    } finally {
        lock.unlock();
    }
}

В реальных проектах я использую инструменты для мониторинга: JProfiler, YourKit для обнаружения deadlock'ов и голодания потоков. Также пишу unit тесты с искусственной нагрузкой для выявления проблем параллелизма.

Как решить проблему голодания потоков? | PrepBro