Перейти к основному содержимому

Руководство по java.util.concurrent.Locks

· 6 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

Проще говоря, блокировка — это более гибкий и сложный механизм синхронизации потоков, чем стандартный синхронизированный блок.

Интерфейс Lock существует со времен Java 1.5. Он определен внутри пакета java.util.concurrent.lock и предоставляет обширные операции для блокировки.

В этой статье мы рассмотрим различные реализации интерфейса блокировки и их приложения.

2. Различия между блокировкой и синхронизированным блоком

Есть несколько различий между использованием синхронизированного блока и использованием API блокировки :

  • Синхронизированный блок полностью содержится в методе — мы можем иметь операции lock() и unlock() API Lock в отдельных методах .
  • Синхронизированный блок не поддерживает справедливость, любой поток может получить блокировку после освобождения, предпочтения не могут быть указаны. Мы можем добиться справедливости в 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 .