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

Какие синхронизаторы использовал

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

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

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

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

Синхронизаторы в Java: практическое применение

Синхронизаторы — это примитивы многопоточного программирования, которые управляют доступом потоков к общим ресурсам. Рассмотрю основные синхронизаторы, которые я использовал в реальных проектах.

1. synchronized (встроенный монитор)

Когда использовал: для защиты критических секций кода.

public class Counter {
    private int count = 0;
    
    // Синхронизация метода
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
    
    // Или синхронизация блока
    public void incrementFast() {
        synchronized(this) {
            count++;
        }
    }
}

Плюсы:

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

Минусы:

  • Грубая синхронизация (весь объект блокируется)
  • Нет timeout'ов
  • Сложно отлаживать deadlock'и
  • Нет проверки условий (condition waiting)

Пример проблемы:

public synchronized void waitForCondition() {
    while (!conditionMet) {
        // Как дождаться условия? synchronized не подходит
    }
}

2. volatile

Когда использовал: для флагов и простых значений, которые читаются/пишутся несколькими потоками.

public class ShutdownManager {
    private volatile boolean shutdown = false;
    
    public void shutdown() {
        shutdown = true;
    }
    
    public void run() {
        while (!shutdown) {  // Видит изменения из других потоков
            doWork();
        }
    }
}

Плюсы:

  • Минимальный overhead
  • Гарантирует visibility (видимость между потоками)
  • Хорош для флагов

Минусы:

  • Не синхронизирует сложные операции
  • a++ — это НЕ atomic, даже с volatile
public class Counter {
    private volatile long count = 0;
    
    public void increment() {
        count++;  // Опасно! count++ — это 3 операции: read, increment, write
    }
}

3. ReentrantLock

Когда использовал: для более гибкой синхронизации с timeout'ами и условными переменными.

public class LockBasedCounter {
    private long count = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    
    public boolean tryIncrement() {
        if (lock.tryLock()) {  // Не блокирует, если занято
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
    
    public boolean tryIncrementWithTimeout() throws InterruptedException {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}

Плюсы:

  • Более гибкий, чем synchronized
  • tryLock() без блокировки
  • Timeout поддержка
  • Fairness — справедливый порядок

Минусы:

  • Нужно помнить unlock в finally
  • Более сложный для понимания

4. Condition (с ReentrantLock)

Когда использовал: для ожидания условий в потоках.

public class BoundedBuffer<T> {
    private final int capacity;
    private final List<T> items = new ArrayList<>();
    private final Lock lock = new ReentrantLock();
    
    // Условные переменные
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            // Ожидание, пока буфер не пустой
            while (items.size() >= capacity) {
                notFull.await();  // Блокируется, пока буфер полный
            }
            items.add(item);
            notEmpty.signalAll();  // Пробуждаем потоки, ждущие данных
        } finally {
            lock.unlock();
        }
    }
    
    public T take() throws InterruptedException {
        lock.lock();
        try {
            // Ожидание, пока буфер не полный
            while (items.isEmpty()) {
                notEmpty.await();  // Блокируется, пока буфер пуст
            }
            T item = items.remove(0);
            notFull.signalAll();  // Пробуждаем потоки, ждущие места
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Плюсы:

  • Точное управление условиями
  • Несколько conditions на один lock
  • awaitNanos() для timeout'ов

5. CountDownLatch

Когда использовал: для синхронизации завершения нескольких потоков.

public class DataProcessingService {
    
    public void processInParallel(List<Data> dataList) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(dataList.size());
        
        for (Data data : dataList) {
            executorService.submit(() -> {
                try {
                    processData(data);
                } finally {
                    latch.countDown();  // Уменьшить счётчик
                }
            });
        }
        
        latch.await();  // Ждать, пока все потоки завершатся
        System.out.println("Все данные обработаны");
    }
}

Плюсы:

  • Просто использовать
  • Гарантирует завершение
  • One-time use

Минусы:

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

6. CyclicBarrier

Когда использовал: для синхронизации нескольких потоков на барьере.

public class ParallelComputation {
    
    public static void main(String[] args) throws InterruptedException {
        int numThreads = 4;
        CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
            System.out.println("Все потоки достигли барьера");
        });
        
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        
        for (int i = 0; i < numThreads; i++) {
            final int threadNum = i;
            executor.submit(() -> {
                try {
                    System.out.println("Поток " + threadNum + " работает");
                    Thread.sleep(1000 * (threadNum + 1));
                    System.out.println("Поток " + threadNum + " ждёт на барьере");
                    barrier.await();  // Ждут все потоки друг друга
                    System.out.println("Поток " + threadNum + " прошёл барьер");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

Плюсы:

  • Переиспользуемый (в отличие от CountDownLatch)
  • Можно запустить action'ы при достижении барьера

Минусы:

  • Более сложный, чем CountDownLatch

7. Semaphore

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

public class ConnectionPool {
    private final Semaphore semaphore;
    private final List<Connection> availableConnections;
    
    public ConnectionPool(int poolSize) {
        this.semaphore = new Semaphore(poolSize);
        this.availableConnections = new CopyOnWriteArrayList<>();
        // Инициализация poolSize соединений
    }
    
    public Connection acquireConnection() throws InterruptedException {
        semaphore.acquire();  // Ждёт, пока есть свободное соединение
        return availableConnections.remove(0);
    }
    
    public void releaseConnection(Connection connection) {
        availableConnections.add(connection);
        semaphore.release();  // Освобождает место
    }
}

// Использование
ConnectionPool pool = new ConnectionPool(5);  // Макс 5 соединений
Connection conn = pool.acquireConnection();
try {
    // Использование соединения
} finally {
    pool.releaseConnection(conn);
}

Плюсы:

  • Ограничение одновременных потоков
  • Справедливое распределение

Минусы:

  • Нужно явно освобождать

8. AtomicInteger, AtomicLong, AtomicReference

Когда использовал: для простых атомарных операций без явной синхронизации.

public class AtomicCounter {
    private final AtomicLong count = new AtomicLong(0);
    
    public void increment() {
        count.incrementAndGet();  // Атомарная операция
    }
    
    public long get() {
        return count.get();
    }
    
    public long getAndAddLargeValue(long value) {
        return count.getAndAdd(value);  // Атомарно
    }
    
    public boolean compareAndSet(long expect, long update) {
        return count.compareAndSet(expect, update);  // CAS операция
    }
}

// Более сложный пример
public class AtomicReferenceExample {
    private final AtomicReference<User> currentUser = new AtomicReference<>();
    
    public void setUser(User user) {
        currentUser.set(user);
    }
    
    public User getAndReplaceUser(User newUser) {
        return currentUser.getAndSet(newUser);
    }
}

Плюсы:

  • Нет явных locks
  • Высокая производительность
  • CAS (Compare-And-Swap) операции

9. Collections.synchronizedList/Map

Когда использовал: для быстрого предания списков потокобезопасными.

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

Минусы:

  • Итерация требует явной синхронизации
  • Медленнее, чем concurrent коллекции

10. ConcurrentHashMap

Когда использовал: для потокобезопасных кэшей и конкурентных операций.

public class ConcurrentCache<K, V> {
    private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
    
    public V get(K key) {
        return cache.get(key);
    }
    
    public void put(K key, V value) {
        cache.put(key, value);
    }
    
    // putIfAbsent — атомарная операция
    public V putIfAbsentAndCompute(K key, Function<K, V> computeFunction) {
        return cache.computeIfAbsent(key, computeFunction);
    }
}

Плюсы:

  • Segment-based locking (несколько locks)
  • Лучше для читающих потоков
  • Атомарные операции (putIfAbsent, compute)

Выбор синхронизатора

ЗадачаСинхронизаторПричина
Простой флагvolatileМинимальный overhead
СчётчикAtomicLongВысокая производительность
Критическая секцияsynchronizedПросто
Timeout требуетсяReentrantLockГибкость
Ожидание условияConditionТочное управление
Ждать завершенияCountDownLatchПростота
Барьер потоковCyclicBarrierПереиспользуемость
Ограничить потокиSemaphoreПулирование ресурсов
Потокобезопасный кэшConcurrentHashMapПараллельность

Общие рекомендации

  1. Начни с самого простого (synchronized, volatile)
  2. Профилируй перед оптимизацией (synchronized часто достаточно)
  3. Избегай deadlock'ов (всегда одинаковый порядок захвата locks)
  4. Используй high-level структуры (java.util.concurrent)
  5. Помни о visibility (volatile или synchronized для гарантии)
  6. Concurrent коллекции лучше, чем synchronized (ConcurrentHashMap вместо synchronized Map)
  7. Тестируй многопоточность (Thread timing сложен для воспроизведения)
Какие синхронизаторы использовал | PrepBro