1. Обзор
Проще говоря, блокировка — это более гибкий и сложный механизм синхронизации потоков, чем стандартный синхронизированный
блок.
Интерфейс Lock
существует со времен Java 1.5. Он определен внутри пакета java.util.concurrent.lock
и предоставляет обширные операции для блокировки.
В этой статье мы рассмотрим различные реализации интерфейса блокировки и их приложения.
2. Различия между блокировкой и синхронизированным блоком
Есть несколько различий между использованием синхронизированного блока
и использованием API блокировки :
- Синхронизированный
блок
полностью содержится в методе —операции
lock()
иunlock()
APILock
в отдельных методах . - Синхронизированный
блок
не поддерживает справедливость, любой поток может получить блокировку после освобождения, предпочтения не могут быть указаны. Мы можем добиться справедливости в API-интерфейсахблокировки
, указав свойствосправедливости
. Это гарантирует, что самому длинному ожидающему потоку будет предоставлен доступ к блокировке. - Поток блокируется, если он не может получить доступ к синхронизированному
блоку
. APIблокировки
предоставляетметод tryLock()
. Поток получает блокировку только в том случае, если он доступен и не удерживается каким-либо другим потоком. Это уменьшает время блокировки потока, ожидающего блокировки. - Поток, который находится в состоянии «ожидания» получения доступа к
синхронизированному блоку
, не может быть прерван. APIблокировки
предоставляет методlockInterruptably()
, который можно использовать для прерывания потока, когда он ожидает блокировки.
3. Блокировка
API
Давайте посмотрим на методы в интерфейсе блокировки :
void lock() —
получить блокировку, если она доступна; если блокировка недоступна, поток блокируется до тех пор, пока блокировка не будет снятаvoid lockInterruptably()
— аналогиченlock(),
но позволяет прервать заблокированный поток и возобновить выполнение с помощью выброшенногоисключения java.lang.InterruptedException .
boolean tryLock()
– это неблокирующая версия методаlock()
; он пытается немедленно получить блокировку, возвращает true, если блокировка успешнаboolean tryLock(long timeout, TimeUnit timeUnit) —
это похоже наtryLock(),
за исключением того, что он ждет заданный тайм-аут, прежде чем отказаться от попытки получитьблокировку .
- void
unlock()
— разблокирует экземплярLock
Заблокированный экземпляр всегда должен быть разблокирован, чтобы избежать взаимоблокировки. Рекомендуемый блок кода для использования блокировки должен содержать блоки try/catch
и finally
:
Lock lock = ...;
lock.lock();
try {
// access to the shared resource
} finally {
lock.unlock();
}
В дополнение к интерфейсу Lock
у
нас есть интерфейс ReadWriteLock
, который поддерживает пару блокировок: одну для операций только для чтения и одну для операции записи. Блокировка чтения может одновременно удерживаться несколькими потоками, пока нет записи.
ReadWriteLock
объявляет методы для получения блокировки чтения или записи:
Lock readLock() —
возвращает блокировку, которая использовалась для чтенияLock writeLock()
— возвращает блокировку, которая использовалась для записи.
4. Блокировка реализации
4.1. ReentrantLock
Класс ReentrantLock
реализует интерфейс блокировки .
Он предлагает ту же семантику параллелизма и памяти, что и неявная блокировка монитора, доступ к которой осуществляется с помощью синхронизированных
методов и операторов, с расширенными возможностями.
Давайте посмотрим, как мы можем использовать ReenrtantLock для
синхронизации:
public class SharedObject {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;
public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}
Нам нужно убедиться, что мы оборачиваем вызовы lock
() и unlock() в блок
try-finally
, чтобы избежать тупиковых ситуаций.
Давайте посмотрим, как работает tryLock()
:
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}
В этом случае поток, вызывающий tryLock(),
будет ждать одну секунду и прекратит ожидание, если блокировка недоступна.
4.2. ReentrantReadWriteLock
Класс ReentrantReadWriteLock
реализует интерфейс ReadWriteLock .
Давайте посмотрим на правила получения ReadLock
или WriteLock
потоком:
- Блокировка чтения — если ни один поток не получил блокировку записи или не запросил ее, тогда несколько потоков могут получить блокировку чтения.
- Блокировка записи — если ни один поток не читает и не пишет, только один поток может получить блокировку записи.
Давайте посмотрим, как использовать ReadWriteLock
:
public class SynchronizedHashMapWithReadWriteLock {
Map<String,String> syncHashMap = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
// ...
Lock writeLock = lock.writeLock();
public void put(String key, String value) {
try {
writeLock.lock();
syncHashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
...
public String remove(String key){
try {
writeLock.lock();
return syncHashMap.remove(key);
} finally {
writeLock.unlock();
}
}
//...
}
Для обоих методов записи нам нужно окружить критическую секцию блокировкой записи, доступ к ней может получить только один поток:
Lock readLock = lock.readLock();
//...
public String get(String key){
try {
readLock.lock();
return syncHashMap.get(key);
} finally {
readLock.unlock();
}
}
public boolean containsKey(String key) {
try {
readLock.lock();
return syncHashMap.containsKey(key);
} finally {
readLock.unlock();
}
}
Для обоих методов чтения нам нужно окружить критическую секцию блокировкой чтения. Несколько потоков могут получить доступ к этому разделу, если не выполняется операция записи.
4.3. Штампедлок
StampedLock
представлен в Java 8. Он также поддерживает блокировки чтения и записи. Однако методы получения блокировки возвращают отметку, которая используется для снятия блокировки или для проверки того, действительна ли блокировка:
public class StampedLockDemo {
Map<String,String> map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
Еще одна функция StampedLock
— оптимистическая блокировка. В большинстве случаев операциям чтения не нужно ждать завершения операции записи, и в результате этого не требуется полноценная блокировка чтения.
Вместо этого мы можем перейти на блокировку чтения:
public String readWithOptimisticLock(String key) {
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlock(stamp);
}
}
return value;
}
5. Работа с условиями
Класс Condition
предоставляет потоку возможность ожидать выполнения некоторого условия при выполнении критической секции.
Это может произойти, когда поток получает доступ к критической секции, но не имеет необходимого условия для выполнения своей операции. Например, поток чтения может получить доступ к блокировке общей очереди, в которой все еще нет данных для потребления.
Традиционно Java предоставляет методы wait(), notify() и notifyAll()
для взаимодействия потоков. Условия
имеют схожие механизмы, но кроме того, мы можем указать несколько условий:
public class ReentrantLockWithCondition {
Stack<String> stack = new Stack<>();
int CAPACITY = 5;
ReentrantLock lock = new ReentrantLock();
Condition stackEmptyCondition = lock.newCondition();
Condition stackFullCondition = lock.newCondition();
public void pushToStack(String item){
try {
lock.lock();
while(stack.size() == CAPACITY) {
stackFullCondition.await();
}
stack.push(item);
stackEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public String popFromStack() {
try {
lock.lock();
while(stack.size() == 0) {
stackEmptyCondition.await();
}
return stack.pop();
} finally {
stackFullCondition.signalAll();
lock.unlock();
}
}
}
6. Заключение
В этой статье мы рассмотрели различные реализации интерфейса Lock и недавно представленный класс
StampedLock
. Мы также изучили, как можно использовать класс Condition
для работы с несколькими условиями.
Полный код этого руководства доступен на GitHub .