DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Coding
  3. Java
  4. Simplifying ReadWriteLock with Java 8 and Lambdas

Simplifying ReadWriteLock with Java 8 and Lambdas

Tomasz Nurkiewicz user avatar by
Tomasz Nurkiewicz
CORE ·
Mar. 19, 14 · Interview
Like (0)
Save
Tweet
Share
7.21K Views

Join the DZone community and get the full member experience.

Join For Free

Considering legacy Java code, no matter where you look, Java 8 with lambda expressions can definitely improve quality and readability. Today let us look at ReadWriteLock and how we can make using it simpler. Suppose we have a class called Buffer that remembers last couple of messages in a queue, counting and discarding old ones. The implementation is quite straightforward:

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

Now we can putItem() many times, but the internal recent queue will only keep lastcapacity elements. However it also remembers how many items it had to discard to avoid memory leak. This class works fine, but only in single-threaded environment. We use not thread-safe ArrayDeque and non-synchronized int. While reading and writing toint is atomic, changes are not guaranteed to be visible in different threads. Also even if we use thread safe BlockingDeque together with AtomicInteger we are still in danger of race condition because those two variables aren't synchronized with each other.

One approach would be to synchronize all the methods, but that seems quite restrictive. Moreover we suspect that reads greatly outnumber writes. In such cases ReadWriteLockis a fantastic alternative. It actually consists of two locks - one for reading and one for writing. In reality they both compete for the same lock which can be obtained either by one writer or multiple readers at the same time. So we can have concurrent reads when no one is writing and only occasionally writer blocks all readers. Using synchronized will just always block all the others, no matter what they do. The sad part of ReadWriteLock is that it introduces a lot of boilerplate. You have to explicitly open a lock and remember tounlock() it in finally block. Our implementation becomes hard to read:

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

This is how it was done pre-Java 8. Effective, safe and... ugly. However with lambda expressions we can wrap cross-cutting concerns in a utility class like this:

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

As you can see we wrap ReadWriteLock and provide a set of utility methods to work with. In principle we would like to pass a Runnable orSupplier<T> (interface having single T get() method) and make sure calling it is surrounded with proper lock. We could write the exact same wrapper class without lambdas, but having them greatly simplifies client code:

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

See how we invoke guard.read() and guard.write() passing pieces of code that should be guarded? Looks quite neat. BTW have you noticed how we can turn any collection into any other collection (here: Deque into List) using stream()? Now if we extract couple of internal methods we can use method references to even further simplify lambdas:

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

This is just one of the many ways you can improve existing code and libraries by taking advantage of lambda expressions. We should be really happy that they finally made their way into Java language - while being already present in dozens of other JVM languages.

Java (programming language)

Published at DZone with permission of Tomasz Nurkiewicz, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • SAST: How Code Analysis Tools Look for Security Flaws
  • Automated Performance Testing With ArgoCD and Iter8
  • Top Authentication Trends to Watch Out for in 2023
  • Writing a Modern HTTP(S) Tunnel in Rust

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: