Учитывая устаревший код 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 и соседстве . |