Учитывая устаревший код Java, где бы вы ни находились, Java 8 с лямбда-выражениями определенно может улучшить качество и удобочитаемость. Сегодня давайте посмотрим на ReadWriteLock
и как мы можем сделать его проще. Предположим, у нас есть класс Buffer
который запоминает последние пару сообщений в очереди, считая и отбрасывая старые. Реализация довольно проста:
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
34
35
36
37
38
39
|
public class Buffer { private final int capacity; private final Deque<String> recent; private int discarded; public Buffer( int capacity) { this .capacity = capacity; this .recent = new ArrayDeque<>(capacity); } public void putItem(String item) { while (recent.size() >= capacity) { recent.removeFirst(); ++discarded; } recent.addLast(item); } public List<String> getRecent() { final ArrayList<String> result = new ArrayList<>(); result.addAll(recent); return result; } public int getDiscardedCount() { return discarded; } public int getTotal() { return discarded + recent.size(); } public void flush() { discarded += recent.size(); recent.clear(); } } |
Теперь мы можем putItem()
много раз, но внутренняя recent
очередь будет сохранять только последние элементы capacity
. Однако он также запоминает, сколько предметов пришлось выбросить, чтобы избежать утечки памяти. Этот класс работает нормально, но только в однопоточной среде. Мы используем не потокобезопасный ArrayDeque
и несинхронизированный int
. Хотя чтение и запись в int
атомарны, изменения не гарантируются, чтобы быть видимыми в разных потоках. Кроме того, даже если мы используем потокобезопасный BlockingDeque
вместе с AtomicInteger
мы все еще в опасности состояния гонки, потому что эти две переменные не синхронизированы друг с другом.
Один из подходов заключается в synchronize
всех методов , но это кажется довольно ограничительным. Более того, мы подозреваем, что число операций чтения значительно превышает количество записей В таких случаях ReadWriteLock
— фантастическая альтернатива. На самом деле он состоит из двух замков — один для чтения и один для записи. В действительности они оба борются за один и тот же замок, который может быть получен одним писателем или несколькими читателями одновременно. Таким образом, у нас может быть одновременное чтение, когда никто не пишет, и только иногда писатель блокирует всех читателей. Использование synchronized
всегда будет блокировать все остальные, независимо от того, что они делают. Печальная часть ReadWriteLock
состоит в том, что он представляет много шаблонов. Вы должны явно открыть блокировку и не забыть unlock()
ее в блоке finally
. Наша реализация становится трудно читать:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public class Buffer { private final int capacity; private final Deque<String> recent; private int discarded; private final Lock readLock; private final Lock writeLock; public Buffer( int capacity) { this .capacity = capacity; recent = new ArrayDeque<>(capacity); final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); readLock = rwLock.readLock(); writeLock = rwLock.writeLock(); } public void putItem(String item) { writeLock.lock(); try { while (recent.size() >= capacity) { recent.removeFirst(); ++discarded; } recent.addLast(item); } finally { writeLock.unlock(); } } public List<String> getRecent() { readLock.lock(); try { final ArrayList<String> result = new ArrayList<>(); result.addAll(recent); return result; } finally { readLock.unlock(); } public int getDiscardedCount() { readLock.lock(); try { return discarded; } finally { readLock.unlock(); } } public int getTotal() { readLock.lock(); try { return discarded + recent.size(); } finally { readLock.unlock(); } } public void flush() { writeLock.lock(); try { discarded += recent.size(); recent.clear(); } finally { writeLock.unlock(); } } } |
Вот как это было сделано до Jave 8. Эффективно, безопасно и … безобразно. Однако с помощью лямбда-выражений мы можем обернуть сквозные вопросы в служебный класс следующим образом:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public class FunctionalReadWriteLock { private final Lock readLock; private final Lock writeLock; public FunctionalReadWriteLock() { this ( new ReentrantReadWriteLock()); } public FunctionalReadWriteLock(ReadWriteLock lock) { readLock = lock.readLock(); writeLock = lock.writeLock(); } public <T> T read(Supplier<T> block) { readLock.lock(); try { return block.get(); } finally { readLock.unlock(); } } public void read(Runnable block) { readLock.lock(); try { block.run(); } finally { readLock.unlock(); } } public <T> T write(Supplier<T> block) { writeLock.lock(); try { return block.get(); } finally { writeLock.unlock(); } public void write(Runnable block) { writeLock.lock(); try { block.run(); } finally { writeLock.unlock(); } } } |
Как вы можете видеть, мы обертываем ReadWriteLock
и предоставляем набор утилитарных методов для работы. В принципе, мы хотели бы передать Runnable
или Supplier<T>
(интерфейс с одним методом T get()
) и убедиться, что вызывающий его окружен надлежащей блокировкой. Мы могли бы написать точно такой же класс-обертку без лямбд, но наличие их значительно упрощает клиентский код:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public class Buffer { private final int capacity; private final Deque<String> recent; private int discarded; private final FunctionalReadWriteLock guard; public Buffer( int capacity) { this .capacity = capacity; recent = new ArrayDeque<>(capacity); guard = new FunctionalReadWriteLock(); } public void putItem(String item) { guard.write(() -> { while (recent.size() >= capacity) { recent.removeFirst(); ++discarded; } recent.addLast(item); }); } public List<String> getRecent() { return guard.read(() -> { return recent.stream().collect(toList()); }); } public int getDiscardedCount() { return guard.read(() -> discarded); } public int getTotal() { return guard.read(() -> discarded + recent.size()); } public void flush() { guard.write(() -> { discarded += recent.size(); recent.clear(); }); } } |
Посмотрите, как мы вызываем guard.read()
и guard.write()
передавая фрагменты кода, которые должны быть защищены? Выглядит довольно аккуратно. Кстати, вы заметили, как мы можем превратить любую коллекцию в любую другую коллекцию (здесь: Deque
List
), используя stream()
? Теперь, если мы извлечем пару внутренних методов, мы можем использовать ссылки на методы, чтобы еще больше упростить лямбда-выражения:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public void flush() { guard.write( this ::unsafeFlush); } private void unsafeFlush() { discarded += recent.size(); recent.clear(); } public List<String> getRecent() { return guard.read( this ::defensiveCopyOfRecent); } private List<String> defensiveCopyOfRecent() { return recent.stream().collect(toList()); } |
Это только один из многих способов улучшить существующий код и библиотеки, используя лямбда-выражения. Мы должны быть по-настоящему счастливы, что они наконец-то пробились на язык Java, хотя уже присутствовали на десятках других языков JVM.
Ссылка: | Упрощение ReadWriteLock с Java 8 и лямбдами от нашего партнера по JCG Томаша Нуркевича в блоге о Java и соседстве . |