Какие типы блокировок используются в многопоточности?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Типы блокировок в многопоточности Java
Блокировки — основной инструмент для синхронизации потоков и защиты общих ресурсов от race conditions. Рассмотрю основные типы, механизмы и примеры использования.
1. Intrinsic Locks (Встроенные блокировки)
Каждый объект в Java имеет встроенную блокировку, управляемую через ключевое слово synchronized.
Синхронизация методов
public class Counter {
private int count = 0;
// ✅ Синхронизированный метод
public synchronized void increment() {
count++; // Защищён встроенной блокировкой объекта
}
public synchronized int getCount() {
return count;
}
}
// Использование
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
counter.increment(); // Потокобезопасно
}
Синхронизация блоков
public class SharedResource {
private int data = 0;
private Object lock = new Object();
public void update() {
// Критическая секция
synchronized (lock) {
data++; // Защищена
data *= 2;
}
// Некритичная секция
System.out.println("Updated"); // Не защищена
}
public int getData() {
synchronized (lock) {
return data; // Чтение также защищено
}
}
}
Проблемы встроенных блокировок
// ❌ Потенциальный deadlock
public class Account {
private synchronized void transfer(Account other, int amount) {
this.balance -= amount;
other.receiveTransfer(amount); // Другой объект блокируется!
// Если другой Account ждёт этот, произойдёт deadlock
}
}
// ❌ Нет timeout
synchronized (lock) {
// Если другой поток не освобождает lock, ждём вечно
}
// ❌ Нет справедливости (fairness)
// Потоки могут конкурировать несправедливо
2. Explicit Locks (Lock interface)
Интерфейс Lock из пакета java.util.concurrent.locks предоставляет более гибкий контроль.
ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private double balance;
private Lock lock = new ReentrantLock(); // ✅ Явная блокировка
public void transfer(BankAccount other, double amount) {
// Получаем блокировку с timeout
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // ✅ Timeout
try {
if (other.lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// Безопасная передача
this.balance -= amount;
other.balance += amount;
} finally {
other.lock.unlock();
}
} else {
System.out.println("Cannot acquire other lock");
}
} finally {
lock.unlock();
}
} else {
System.out.println("Transfer timeout");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
ReadWriteLock
ReadWriteLock позволяет множественным потокам читать одновременно, но исключительный доступ для записи.
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CachedData {
private String data;
private ReadWriteLock lock = new ReentrantReadWriteLock();
// Много потоков могут читать одновременно
public String read() {
lock.readLock().lock(); // ✅ Неисключительная блокировка
try {
return data;
} finally {
lock.readLock().unlock();
}
}
// Только один поток может писать
public void write(String newData) {
lock.writeLock().lock(); // ✅ Исключительная блокировка
try {
data = newData;
} finally {
lock.writeLock().unlock();
}
}
}
// Использование
CachedData cache = new CachedData();
// Множество потоков читают параллельно
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> System.out.println(cache.read()));
}
// Писатель ждёт, пока все читатели закончат
executor.submit(() -> cache.write("New data"));
3. Atomics (Атомарные переменные)
Для простых операций с примитивами лучше использовать Atomic классы* без явных блокировок.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicCounter {
// ✅ Потокобезопасно без synchronized
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Атомарная операция
}
public int getCount() {
return count.get();
}
public boolean compareAndSet(int expected, int newValue) {
// ✅ CAS (Compare-And-Swap) операция
return count.compareAndSet(expected, newValue);
}
}
// Пример использования CAS
public class RetryableOperation {
private AtomicInteger retryCount = new AtomicInteger(0);
public void retryWithBackoff() {
int attempt = 0;
while (attempt < 3) {
try {
// Выполняем операцию
if (retryCount.compareAndSet(attempt, attempt + 1)) {
System.out.println("Attempt " + (attempt + 1));
// Успешно увеличили счётчик
break;
}
attempt++;
} catch (Exception e) {
attempt++;
}
}
}
}
4. StampedLock
StampedLock — оптимизированная версия ReadWriteLock с поддержкой оптимистического чтения.
import java.util.concurrent.locks.StampedLock;
public class OptimizedCache {
private String data;
private StampedLock lock = new StampedLock();
// Оптимистическое чтение (очень быстро)
public String readOptimistic() {
long stamp = lock.tryOptimisticRead(); // ✅ Без блокировки
String result = data;
// Проверяем, не изменилось ли
if (!lock.validate(stamp)) {
// Если изменилось, переходим на обычное чтение
stamp = lock.readLock();
try {
result = data;
} finally {
lock.unlockRead(stamp);
}
}
return result;
}
// Исключительная запись
public void write(String newData) {
long stamp = lock.writeLock();
try {
data = newData;
} finally {
lock.unlockWrite(stamp);
}
}
}
5. Semaphore (Семафор)
Semaphore ограничивает количество потоков, имеющих доступ к ресурсу.
import java.util.concurrent.Semaphore;
public class ConnectionPool {
private Semaphore semaphore;
private List<Connection> connections;
public ConnectionPool(int poolSize) {
semaphore = new Semaphore(poolSize); // Максимум poolSize потоков
connections = new ArrayList<>();
for (int i = 0; i < poolSize; i++) {
connections.add(new Connection());
}
}
public Connection acquireConnection() throws InterruptedException {
semaphore.acquire(); // ✅ Ждём свободного слота
synchronized (connections) {
return connections.remove(0);
}
}
public void releaseConnection(Connection conn) {
synchronized (connections) {
connections.add(conn);
}
semaphore.release(); // ✅ Освобождаем слот
}
}
// Использование
ConnectionPool pool = new ConnectionPool(5);
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try {
Connection conn = pool.acquireConnection();
try {
// Используем соединение
System.out.println("Using connection");
Thread.sleep(100);
} finally {
pool.releaseConnection(conn);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
6. CountDownLatch
CountDownLatch позволяет потокам ждать, пока другие потоки завершат свои задачи.
import java.util.concurrent.CountDownLatch;
public class RaceStarterExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(3);
// 3 потока ждут сигнала старта
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
startSignal.await(); // ✅ Ждём сигнала
System.out.println(Thread.currentThread().getName() + " is running");
Thread.sleep(100);
doneSignal.countDown(); // ✅ Уменьшаем счётчик
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
System.out.println("Starting race...");
startSignal.countDown(); // ✅ Даём сигнал старта
doneSignal.await(); // ✅ Ждём, пока все закончат
System.out.println("Race finished");
}
}
7. CyclicBarrier
CyclicBarrier — точка синхронизации, где потоки встречаются.
import java.util.concurrent.CyclicBarrier;
public class BarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(
3, // 3 потока должны встретиться
() -> System.out.println("All threads reached barrier!") // Колбэк
);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
System.out.println("Thread " + id + " is waiting");
barrier.await(); // ✅ Ждём других потоков
System.out.println("Thread " + id + " is proceeding");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
Сравнение типов блокировок
| Тип | Случай использования | Pros | Cons |
|---|---|---|---|
| Synchronized | Простая защита | Встроённо, просто | Нет timeout, нет fairness |
| ReentrantLock | Сложные сценарии | Timeout, fairness, гибкость | Нужно unlock в finally |
| ReadWriteLock | Много читателей | Параллельное чтение | Overhead для письма |
| AtomicInteger | Простые числа | Быстро, без явных lock | Только примитивы |
| StampedLock | Оптимизм важен | Очень быстро | Сложнее в использовании |
| Semaphore | Ограничение потоков | Легко контролировать | Нужна ручная работа |
| CountDownLatch | Синхронизация старта | Простая координация | Одноразовая |
| CyclicBarrier | Встречи потоков | Переиспользуемая | Более сложная |
Лучшие практики
// ✅ Предпочитай AtomicInteger для простых счётчиков
private AtomicInteger counter = new AtomicInteger(0);
// ✅ Используй ReentrantLock для сложной логики
private Lock lock = new ReentrantLock();
lock.lock();
try {
// Критический код
} finally {
lock.unlock(); // Обязательно в finally!
}
// ✅ ReadWriteLock когда много читателей
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
// ❌ Избегай вложенных блокировок
synchronized (a) {
synchronized (b) { // Риск deadlock
}
}
// ❌ Не блокируй во время I/O
synchronized (this) {
httpClient.get(url); // Плохо: другие потоки ждут I/O
}
Заключение
Java предоставляет богатый набор инструментов для синхронизации:
- Synchronized — для простых случаев
- ReentrantLock — для control + timeout
- ReadWriteLock — для read-heavy scenarios
- Atomics — для примитивных типов
- Semaphore/CountDownLatch/CyclicBarrier — для координации потоков
Выбор правильного механизма зависит от требований вашей задачи.