Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Синхронизаторы в 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 | Параллельность |
Общие рекомендации
- Начни с самого простого (synchronized, volatile)
- Профилируй перед оптимизацией (synchronized часто достаточно)
- Избегай deadlock'ов (всегда одинаковый порядок захвата locks)
- Используй high-level структуры (java.util.concurrent)
- Помни о visibility (volatile или synchronized для гарантии)
- Concurrent коллекции лучше, чем synchronized (ConcurrentHashMap вместо synchronized Map)
- Тестируй многопоточность (Thread timing сложен для воспроизведения)