Но иногда нам нужно больше контроля над синхронизацией. Либо нам нужно отдельно управлять типами доступа (чтение и запись), либо использовать его громоздко, потому что явного мьютекса нет, либо нам нужно поддерживать несколько мьютексов.
К счастью, в 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 .
- Учебник по параллелизму Java — Семафоры
- Учебник по параллелизму Java — Пулы потоков
- Учебник по параллелизму Java — Callable, будущее
- Учебник по параллелизму Java — Блокировка очередей
- Учебник по параллелизму Java — CountDownLatch
- Exchanger и Java без GC
- Java Fork / Join для параллельного программирования
- Лучшие практики Java — битва в очереди и связанный ConcurrentHashMap
- Как избежать ConcurrentModificationException при использовании итератора
- Советы по повышению производительности приложений Java