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

Какие знаешь способы упорядочить работу потоков?

2.7 Senior🔥 71 комментариев
#Многопоточность

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

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

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

Способы упорядочить работу потоков (Thread Synchronization)

Когда несколько потоков работают с одними и теми же данными, возникают race conditions и deadlocks. Java предоставляет несколько механизмов синхронизации потоков для контролируемого доступа к общим ресурсам.

1. synchronized блок (мониторы)

Это базовый механизм синхронизации. Только один поток может одновременно выполнять код внутри synchronized блока.

// Плохо: несинхронизированный доступ
public class BankAccount {
    private int balance = 0;
    
    public void deposit(int amount) {
        balance += amount;  // Race condition! Потеря данных
    }
    
    public int getBalance() {
        return balance;
    }
}

// Хорошо: synchronized блок
public class BankAccount {
    private int balance = 0;
    private Object lock = new Object();
    
    public void deposit(int amount) {
        synchronized(lock) {
            // Только один поток может быть здесь одновременно
            balance += amount;
        }
    }
    
    public int getBalance() {
        synchronized(lock) {
            return balance;
        }
    }
}

// Или синхронизировать весь метод
public class BankAccount {
    private int balance = 0;
    
    public synchronized void deposit(int amount) {
        balance += amount;
    }
    
    public synchronized int getBalance() {
        return balance;
    }
}

Плюсы:

  • Простой способ
  • Встроенный механизм языка
  • Автоматическое освобождение блокировки (даже при исключении)

Минусы:

  • Deadlock риск при несколько synchronized блоков
  • Сложно отлаживать
  • Производительность хуже при высокой конкуренции
  • Нельзя разблокировать рано (нет timeout)

2. ReentrantLock (более гибкая альтернатива)

ReentrantLock позволяет более точно управлять блокировкой.

import java.util.concurrent.locks.ReentrantLock;

public class BankAccount {
    private int balance = 0;
    private ReentrantLock lock = new ReentrantLock();
    
    public void deposit(int amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();  // ОБЯЗАТЕЛЬНО в finally!
        }
    }
    
    // С timeout
    public boolean tryDeposit(int amount, long timeoutMs) throws InterruptedException {
        if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
            try {
                balance += amount;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;  // Не получил блокировку вовремя
    }
}

Плюсы:

  • Timeout поддержка (избегаем deadlock)
  • tryLock() — неблокирующая попытка
  • Условные переменные (Condition)
  • Справедливость (fairness)

Минусы:

  • Требует явного unlock()
  • Более объёмный код

3. ReadWriteLock (для чтения-писания)

Если много потоков читают, но редко пишут — ReadWriteLock даёт лучшую производительность.

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheWithReadWriteLock {
    private Map<String, String> cache = new HashMap<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    
    // МНОГО потоков могут читать одновременно
    public String get(String key) {
        lock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // ОДИН поток может писать (блокирует все читающие)
    public void put(String key, String value) {
        lock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Плюсы:

  • Высокая производительность при интенсивном чтении
  • Справедливое распределение

Минусы:

  • Медленнее для равного чтения и писания
  • Более сложный код

4. Atomic переменные (lock-free подход)

Для простых операций atomic переменные дают лучшую производительность без явного lock.

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

public class Counter {
    // Вместо int counter + synchronized
    private AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet();  // Атомная операция
    }
    
    public int getCount() {
        return counter.get();
    }
    
    // Compare-and-swap (CAS)
    public boolean compareAndSet(int expect, int update) {
        return counter.compareAndSet(expect, update);
    }
}

// Пример с AtomicReference
public class Node {
    private AtomicReference<String> value = new AtomicReference<>(null);
    
    public String getValue() {
        return value.get();
    }
    
    public void setValue(String newValue) {
        value.set(newValue);
    }
}

Плюсы:

  • Без явного lock (нет deadlock)
  • Очень высокая производительность
  • Работает для примитивов и ссылок

Минусы:

  • Только для простых операций
  • CAS может требовать retry loop

5. Semaphore (семафор)

Управляет доступом к ресурсам, ограничивая количество потоков одновременно.

import java.util.concurrent.Semaphore;

public class DatabaseConnectionPool {
    private Semaphore available = new Semaphore(10);  // 10 соединений
    private List<Connection> connections = new ArrayList<>();
    
    public Connection acquireConnection() throws InterruptedException {
        available.acquire();  // Ждём, пока будет свободное соединение
        synchronized(connections) {
            return connections.remove(0);
        }
    }
    
    public void releaseConnection(Connection conn) {
        synchronized(connections) {
            connections.add(conn);
        }
        available.release();  // Освобождаем семафор
    }
}

Плюсы:

  • Контроль количества потоков
  • Справедливое распределение

Минусы:

  • Требует явного release()

6. CountDownLatch (ждём событий)

Один или больше потоков ждут, пока другие потоки завершат работу.

import java.util.concurrent.CountDownLatch;

public class ServiceStartup {
    private CountDownLatch latch = new CountDownLatch(3);  // Ждём 3 события
    
    public void startServices() throws InterruptedException {
        // Стартуем 3 сервиса в отдельных потоках
        new Thread(() -> {
            startDatabaseService();
            latch.countDown();  // Уменьшаем счётчик
        }).start();
        
        new Thread(() -> {
            startCacheService();
            latch.countDown();
        }).start();
        
        new Thread(() -> {
            startWebServer();
            latch.countDown();
        }).start();
        
        // Ждём, пока все 3 сервиса стартуют
        latch.await();  // Блокирует, пока счётчик != 0
        System.out.println("All services started!");
    }
}

Плюсы:

  • Простой способ синхронизации начального событий
  • Понятный код

Минусы:

  • Одноразовый (нельзя переиспользовать)
  • Нельзя переустановить счётчик

7. CyclicBarrier (циклический барьер)

Некоторое количество потоков ждут друг друга в определённой точке.

import java.util.concurrent.CyclicBarrier;

public class GameServer {
    private CyclicBarrier barrier = new CyclicBarrier(4, () -> {
        System.out.println("Game starting with 4 players!");
    });
    
    public void playerReady(String playerName) throws Exception {
        System.out.println(playerName + " is ready");
        barrier.await();  // Ждёт, пока все 4 игрока будут готовы
        System.out.println(playerName + " game started!");
    }
}

// Использование
for (int i = 0; i < 4; i++) {
    int id = i;
    new Thread(() -> {
        try {
            gameServer.playerReady("Player " + id);
        } catch (Exception e) {}
    }).start();
}

Плюсы:

  • Переиспользуется после каждой волны
  • Хорошо для batch обработки

Минусы:

  • Требует знания количества потоков заранее

8. Phaser (современная альтернатива)

Улучшенная версия CyclicBarrier для фаз работы.

import java.util.concurrent.Phaser;

public class WorkerPool {
    private Phaser phaser = new Phaser(1);  // 1 = этап инициации
    
    public void addWorker(int id) {
        phaser.register();  // Добавляем воркер
        
        new Thread(() -> {
            // Фаза 1: инициализация
            System.out.println("Worker " + id + " starting");
            phaser.arriveAndAwaitAdvance();
            
            // Фаза 2: основная работа
            System.out.println("Worker " + id + " working");
            phaser.arriveAndAwaitAdvance();
            
            // Фаза 3: завершение
            System.out.println("Worker " + id + " done");
            phaser.arriveAndDeregister();
        }).start();
    }
}

Плюсы:

  • Более гибкий, чем CyclicBarrier
  • Может быть зарегистрирована динамически

9. BlockingQueue (очередь с синхронизацией)

Отличный способ для producer-consumer паттерна.

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class OrderProcessor {
    private BlockingQueue<Order> queue = new LinkedBlockingQueue<>(100);
    
    // Producer
    public void submitOrder(Order order) throws InterruptedException {
        queue.put(order);  // Блокирует, если очередь полна
    }
    
    // Consumer
    public Order processNextOrder() throws InterruptedException {
        return queue.take();  // Ждёт, если очередь пуста
    }
}

Плюсы:

  • Встроенная синхронизация
  • Идеально для асинхронной обработки
  • Автоматическое управление потоками

10. synchronized Collection vs Concurrent Collections

// Плохо: synchronized collections блокируют весь список
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// Итерация блокирует весь список
for (String item : syncList) {
    // ...
}

// Хорошо: concurrent collections фиксируют только сегмент
List<String> concList = new CopyOnWriteArrayList<>();
// Итерация не блокирует, новые элементы добавляются без блокировки
for (String item : concList) {
    // ...
}

// ConcurrentHashMap вместо synchronized HashMap
Map<String, Integer> concMap = new ConcurrentHashMap<>();
concMap.put("key", 1);  // Не блокирует весь map

Плюсы:

  • Более высокая производительность
  • Фиксируют только нужные сегменты

Чеклист выбора механизма

СценарийМеханизмПричина
Простой счётчикAtomicIntegerLock-free, быстро
Доступ к общему объектуsynchronized / ReentrantLockПросто и надёжно
Много читающих, мало пишущихReadWriteLockВысокая производительность
Ждём завершения N операцийCountDownLatchУдобный API
N потоков синхронизируютсяCyclicBarrier / PhaserПонятно и эффективно
Ограничение одновременных потоковSemaphoreКонтроль ресурсов
Producer-consumerBlockingQueueВстроенная синхронизация

Best practices

  • Избегай synchronized блоков, используй современные инструменты
  • Используй Atomic для примитивных операций
  • Используй BlockingQueue для асинхронной обработки
  • Не держи блокировку долго — отпусти как скорее
  • Избегай вложенных блокировок (deadlock риск)
  • Используй try-finally или try-with-resources
  • Тестируй многопоточность с инструментами (Thread stress test)

Правильный выбор синхронизации — критичен для производительности и надёжности многопоточного приложения!

Какие знаешь способы упорядочить работу потоков? | PrepBro