Статьи

Учебник по параллелизму Java — повторяющиеся блокировки

Синхронизированное ключевое слово Java — замечательный инструмент — он позволяет нам простой и надежный способ синхронизации доступа к критическим разделам, и его не так сложно понять.

Но иногда нам нужно больше контроля над синхронизацией. Либо нам нужно отдельно управлять типами доступа (чтение и запись), либо использовать его громоздко, потому что явного мьютекса нет, либо нам нужно поддерживать несколько мьютексов.

К счастью, в Java 1.5 были добавлены служебные классы блокировки, которые облегчают решение этих проблем.

Java Reentrant Locks

Java имеет несколько реализаций блокировки в пакете java.util.concurrent.locks.

Общие классы замков красиво оформлены как интерфейсы:

  • Замок — самый простой случай замка, который может быть приобретен и освобожден
  • ReadWriteLock — реализация блокировки, которая имеет оба типа блокировки чтения и записи — несколько блокировок чтения могут удерживаться одновременно, если не удерживается исключительная блокировка записи

Java предоставляет две реализации этих блокировок, которые нас интересуют — обе являются реентерабельными (это просто означает, что поток может повторно получить одну и ту же блокировку несколько раз без каких-либо проблем).

  • ReentrantLock — как и следовало ожидать, реентерабельная реализация Lock
  • ReentrantReadWriteLock — реентерабельная реализация ReadWriteLock

Теперь давайте посмотрим несколько примеров.

Пример блокировки чтения / записи

Так как же использовать замок? Это довольно просто: просто приобрети и отпусти (и никогда не забудь выпустить — наконец-то твой друг!).

Представьте, что у нас очень простой случай, когда нам нужно синхронизировать доступ к паре переменных. Один является простым значением, а другой выводится на основе некоторого длительного расчета. Во-первых, это то, как мы выполняем это с синхронизированным ключевым словом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Calculator {
    private int calculatedValue;
    private int value;
 
    public synchronized void calculate(int value) {
        this.value = value;
        this.calculatedValue = doMySlowCalculation(value);
    }
 
    public synchronized int getCalculatedValue() {
        return calculatedValue;
    }
 
    public synchronized int getValue() {
        return value;
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Calculator {
    private int calculatedValue;
    private int value;
    private ReadWriteLock lock = new ReentrantReadWriteLock();
 
    public void calculate(int value) {
        lock.writeLock().lock();
        try {
            this.value = value;
            this.calculatedValue = doMySlowCalculation(value);
        } finally {
            lock.writeLock().unlock();
        }
    }
 
    public int getCalculatedValue() {
        lock.readLock().lock();
        try {
            return calculatedValue;
        } finally {
            lock.readLock().unlock();
        }
    }
 
    public int getValue() {
        lock.readLock().lock();
        try {
            return value;
        } finally {
            lock.readLock().unlock();
        }
    }
}

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

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

Более типичное использование

Наш первый пример может сбить вас с толку или не совсем убедить, что явные блокировки полезны. Разве нет других способов их использования, которые не были так изобретены? Конечно!

Мы в Carfey использовали явные блокировки для решения многих проблем. Один из примеров — когда у вас есть различные задачи, которые могут выполняться одновременно, но вы не хотите, чтобы одновременно выполнялось более одного типа. Один чистый способ реализовать это с помощью замков. Это может быть сделано с синхронизированным, но блокировки дают нам возможность потерпеть неудачу после истечения времени ожидания.

В качестве бонуса вы заметите, что мы использовали сочетание синхронизированных и явных блокировок — иногда одна просто чище и проще, чем другая.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TaskRunner {
    private Map<Class<? extends Runnable>,  Lock> mLocks =
            new HashMap<Class<? extends Runnable>,  Lock>();
 
    public void runTaskUniquely(Runnable r, int secondsToWait) {
        Lock lock = getLock(r.getClass());
        boolean acquired = lock.tryLock(secondsToWait, TimeUnit.SECONDS);
        if (acquired) {
            try {
                r.run();
            } finally {
                lock.unlock();
            }
        } else {
            // failure code here
        }
    }
 
    private synchronized Lock getLock(Class clazz) {
        Lock l = mLocks.get(clazz);
        if (l == null) {
            l = new ReentrantLock();
            mLocks.put(clazz, l);
        }
        return l;
    }
}

Эти два примера должны дать вам довольно хорошее представление о том, как использовать оба плана Locks и ReadWriteLocks . Как и в случае с синхронизированным, не беспокойтесь о повторной блокировке той же блокировки — в блокировках, предоставляемых в JDK, проблем не возникнет, поскольку они повторно вводятся.

Всякий раз, когда вы имеете дело с параллелизмом, существуют опасности. Всегда помните следующее:

  • Отпустите все блокировки в блоке наконец. Это правило 1 по причине.
  • Остерегайтесь нити голодной! Справедливая настройка в ReentrantLocks может быть полезна, если у вас много читателей и случайных писателей, которых вы не хотите ждать вечно. Возможно, писатель мог бы ждать очень долго (возможно, вечно), если другие потоки постоянно блокируют чтение.
  • Используйте синхронизированные, где это возможно. Вы избежите ошибок и сохраните свой код чище.
  • Используйте tryLock (), если вы не хотите, чтобы поток, ожидающий бесконечно, получил блокировку — это похоже на тайм-ауты ожидания блокировки, которые есть у баз данных.

Вот и все! Если у вас есть вопросы или комментарии, не стесняйтесь оставлять их ниже.

Ссылка: Java Concurrency Part 2 — Reentrant Locks от наших партнеров JCG в блоге Carfey Software .

Статьи по Теме :