Статьи

Упрощение ReadWriteLock с Java 8 и лямбдами

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