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

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

 

1. обзор

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

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

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

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

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

  • A synchronized block is fully contained within a method – мы можем иметь операцииLock APIlock() иunlock() в отдельных методах

  • A synchronized block не поддерживает справедливость, любой поток может получить блокировку после снятия, никакие предпочтения не могут быть указаны. We can achieve fairness within the Lock APIs by specifying the fairness property. Это гарантирует, что самый длинный ожидающий поток получит доступ к блокировке

  • Поток блокируется, если он не может получить доступ к синхронизированномуblock. The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. Это сокращает время блокировки потока, ожидающего блокировки

  • Поток, который находится в состоянии «ожидания» получения доступа кsynchronized block, не может быть прерван. The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock

3. Lock API

Давайте посмотрим на методы в интерфейсеLock:

  • *void lock()* – получает блокировку, если она доступна; если блокировка недоступна, поток блокируется до тех пор, пока блокировка не будет снята

  • void lockInterruptibly() - это похоже наlock(),, но позволяет прервать заблокированный поток и возобновить выполнение через брошенныйjava.lang.InterruptedException

  • boolean tryLock() - это неблокирующая версия методаlock(); он пытается получить блокировку немедленно, возвращает истину, если блокировка успешна

  • *boolean tryLock(long timeout, TimeUnit timeUnit)* –, это похоже наtryLock(),, за исключением того, что он ожидает заданный тайм-аут, прежде чем отказаться от попытки получитьLock

  • 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 реализует интерфейсLock. Он предлагает ту же семантику параллелизма и памяти, что и неявная блокировка монитора, доступ к которой осуществляется с помощью методов и операторовsynchronized, с расширенными возможностями.

Давайте посмотрим, как мы можем использовать синхронизациюReenrtantLock for:

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

  • Read Lock - если ни один поток не получил блокировку записи или не запросил ее, тогда несколько потоков могут получить блокировку чтения

  • Write Lock - если ни один поток не читает или не пишет, тогда только один поток может получить блокировку записи

Давайте посмотрим, как использоватьReadWriteLock:

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. Работа сConditions

КлассCondition дает возможность потоку ждать возникновения некоторого условия при выполнении критического раздела.

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

Традиционно Java предоставляет методыwait(), notify() and notifyAll() для взаимодействия потоков. Conditions имеют аналогичные механизмы, но, кроме того, мы можем указать несколько условий:

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 для работы с несколькими условиями.

Полный код этого руководства доступенover on GitHub.