Рубрики
Без рубрики

Руководство по java.util.concurrent.Замки

В этой статье мы рассмотрим различные реализации интерфейса блокировки и недавно введенный в Java 9 класс StampedLock.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

Существует несколько различий между использованием synchronized block и использованием Lock API:

  • /| синхронизированный блок полностью содержится в методе – у нас может быть Блокировка API блокировка() и разблокировка() операция в отдельных методах Поскольку
  • synchronized block не поддерживает справедливость, любой поток может получить блокировку после освобождения, никакие предпочтения не могут быть указаны. Мы можем добиться справедливости в API-интерфейсах Lock , указав свойство fair /. Это гарантирует, что самый длинный ожидающий поток получит доступ к блокировке Поток блокируется, если он не может получить доступ к синхронизированному блоку
  • . API Lock предоставляет метод tryLock () . Поток получает блокировку только в том случае, если он доступен и не удерживается каким-либо другим потоком. Это сокращает время блокировки потока, ожидающего блокировки Поток , находящийся в состоянии “ожидания” для получения доступа к синхронизированному блоку
  • , не может быть прерван. API Lock предоставляет метод lockInterruptibly () , который можно использовать для прерывания потока, когда он ожидает блокировки

3. Блокировка API

Давайте взглянем на методы в интерфейсе Lock :

  • void lock() получить блокировку, если она доступна; если блокировка недоступна, поток блокируется до тех пор, пока блокировка не будет снята
  • void lockInterruptibly() – это похоже на lock(), но это позволяет прервать заблокированный поток и возобновить выполнение через брошенный java.lang.Исключение InterruptedException
  • boolean tryLock() – это неблокирующая версия метода lock () ; он пытается немедленно получить блокировку, возвращает true, если блокировка удалась
  • логический tryLock(длинный тайм – аут, 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 реализует интерфейс Lock . Он предлагает ту же семантику параллелизма и памяти, что и неявная блокировка монитора, доступ к которой осуществляется с помощью методов и операторов synchronized , с расширенными возможностями.

Давайте посмотрим, как мы можем использовать Reentrantlock для синхронизации:

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 потоком:

  • Блокировка чтения – если ни один поток не получил блокировку записи или не запросил ее, то несколько потоков могут получить блокировку чтения
  • Блокировка записи – если ни один поток не читает или не записывает, то только один поток может получить блокировку записи

Давайте посмотрим, как использовать блокировку чтения и записи :

public class SynchronizedHashMapWithReadWriteLock {

    Map 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

StampedLock представлен в Java 8. Он также поддерживает блокировку как чтения, так и записи. Однако методы получения блокировки возвращают штамп, который используется для снятия блокировки или проверки того, действительна ли блокировка:

public class StampedLockDemo {
    Map 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 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 .