In this article, I will first create a thread-safe stack from scratch with a traditional approach, and then I will refactor it with an execute-around design pattern.
Stack is a last-in-first-out
(LIFO) data structure. It has two main methods, push
and pop
. Let’s create it –
package com.bazlur;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrentStack<T> {
private final Lock lock = new ReentrantLock();
private T[] elements;
private int size = -1;
public ConcurrentStack() {
this(10);
}
public ConcurrentStack(int initialCapacity) {
this.elements = (T[]) new Object[initialCapacity];
}
public void push(T value) {
lock.lock();
try {
growIfNeeded();
elements[size] = value;
} finally {
lock.unlock();
}
}
public T pop() {
lock.lock();
try {
if (size == -1) {
throw new NoSuchElementException();
}
trimToSizeIfNeeded();
var element = elements[size];
elements[size] = null;
size--;
return element;
} finally {
lock.unlock();
}
}
private void growIfNeeded() {
if (++size == elements.length) {
grow();
}
}
private void grow() {
int newCapacity = elements.length * 2;
elements = Arrays.copyOf(elements, newCapacity);
}
private void trimToSizeIfNeeded() {
if (size < elements.length) {
elements = Arrays.copyOf(elements, size + 1);
}
}
}
This is the most straightforward implementation, and it has many shortcomings; anyway, that’s beside the point.
Look at the push and pop method; one thing is very common: locking the lock and then unlocking it. This seems boilerplate, and it has nothing to do with the actual business logic of the stack.
Let’s move this boilerplate code to another place-
package com.bazlur;
import java.util.concurrent.locks.Lock;
import java.util.function.Supplier;
public class LockHelper {
public static void withLock(Lock lock, Runnable runnable) {
lock.lock();
try {
runnable.run();
} finally {
lock.unlock();
}
}
public static <T> T withLock(Lock lock, Supplier<T> supplier) {
lock.lock();
try {
return supplier.get();
} finally {
lock.unlock();
}
}
}
The first method takes a lock and a runnable. It does the locking and looking, and in between, it executes the run method of the runnable. The following method also takes a lock and a Supplier. In case we need to return something, that’s why we used Supplier here.
Let’s use these methods in the ConcurrentStack.
package com.bazlur;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrentStack2<T> {
private final Lock lock = new ReentrantLock();
private T[] elements;
private int size = -1;
public ConcurrentStack2() {
this(10);
}
public ConcurrentStack2(int initialCapacity) {
this.elements = (T[]) new Object[initialCapacity];
}
public void push(T value) {
LockHelper.withLock(lock, () -> {
growIfNeeded();
elements[size] = value;
});
}
public T pop() {
return LockHelper.withLock(lock, () -> {
if (size == -1) {
throw new NoSuchElementException();
}
trimToSizeIfNeeded();
var element = elements[size];
elements[size] = null;
size--;
return element;
});
}
private void growIfNeeded() {
if (++size == elements.length) {
grow();
}
}
private void grow() {
int newCapacity = elements.length * 2;
elements = Arrays.copyOf(elements, newCapacity);
}
private void trimToSizeIfNeeded() {
if (size < elements.length) {
elements = Arrays.copyOf(elements, size + 1);
}
}
}
Now our code is much cleaner and doesn’t’t have any of those boilerplates locking related code.
If we want to add another method in this class, we will use a similar pattern. –
public T peek() {
return LockHelper.withLock(lock, () -> elements[size]);
}
We are executing our code around something that seems not present before our eyes, removing noise and bringing clarity. This is why it is called execute around design pattern, and it helps to remove a lot of similar boilerplate code.
That’s for today!
Cheers!!
for copy/paste pleasure: https://github.com/rokon12/100DaysOfJava/blob/main/src/main/java/com/bazlur/ConcurrentStack2.java