Статьи

Упрощение ReadWriteLock с Java 8 и Lambdas

Учитывая устаревший код Java, где бы вы ни находились, Java 8 с лямбда-выражениями определенно может улучшить качество и удобочитаемость. Сегодня давайте посмотрим,  ReadWriteLock как мы можем сделать это проще. Предположим, у нас есть класс,  Buffer который запоминает последние пару сообщений в очереди, считая и отбрасывая старые. Реализация довольно проста:

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 блоке. Наша реализация становится трудно читать:

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(); } 
}
 
p
ublic 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(); } } } 

Вот как это было сделано до Java 8. Эффективно, безопасно и … некрасиво. Однако с помощью лямбда-выражений мы можем обернуть сквозные вопросы в служебный класс следующим образом:

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() метод) и убедиться, что вызывающий его окружен надлежащей блокировкой. Мы могли бы написать точно такой же класс-обертку без лямбд, но наличие их значительно упрощает клиентский код:

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()? Теперь, если мы извлечем пару внутренних методов, мы можем использовать ссылки на методы, чтобы еще больше упростить лямбда-выражения:

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.