Synchronizers
A synchronizer is any object that coordinates and synchronizes the control flow of threads based on its state. The simplest form synchronizer we have already used, being locks.
Semaphore
A semaphore is an integer variable that represents a resource counter which can also be interpreted as a number of permits to access the resource. The main usage for semaphores is to restrict the number of threads than can access some physical or logical resource.
public class Semaphore {
public Semaphore(int permits) {...}
// acquires a permit, blocking until one is available, or the thread is interrupted.
public void acquire() throws InterruptedException {...}
// acquires a permit, blocking until one is available.
public void acquireUninterruptibly() {...}
public void release() {...}
}
It is for example perfect to implement the CarPark Class as previously seen:
class SemaphoreCarPark implements CarPark {
private final Semaphore s;
public SemaphoreCarPark(int places) {
s = new Semaphore(places);
}
public void enter() {
s.acquireUninterruptibly();
log("enter carpark");
}
public void exit() {
log("exit carpark");
s.release();
}
}
Lock Using a Semaphore
A binary semaphore (only holding 1 permit) can be used as a lock. The only problem with this lock is that it isn't reentrant and a different thread can release the lock that was originally acquired by a different thread.
class SemaphoreLock {
private final Semaphore mutex = new Semaphore(1);
public void lock() { mutex.acquireUninterruptibly();}
public void unlock() { mutex.release(); }
}
Read-Write Lock
The motivation for a ReadWriteLock is that if we use the same lock for reading and writing then only one thread can read at a time even tho there wouldn't be any problems if multiple threads could read at a time. To solve this a ReadWriteLock maintains a pair of locks, a lock for reading which can be held simultaneously by multiple readers and a write lock that can only be held by one thread. This leads to there being 2 possible states. Either one thread is writing or one or multiple threads are reading.
public interface ReadWriteLock {
Lock readLock(); // allows for concurrent reads
Lock writeLock(); // writes are exclusive
}
class KeyValueStore {
private final Map<String, Object> m = new TreeMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Object get(String key) {
r.lock(); try { return m.get(key); } finally { r.unlock(); }
}
public Set<String> allKeys() {
r.lock(); try { return new HashSet<>(m.keySet()); } finally { r.unlock(); }
}
public void put(String key, Object value) {
w.lock(); try { m.put(key, value); } finally { w.unlock(); }
}
public void clear() {
w.lock(); try { m.clear(); } finally { w.unlock(); }
}
}
Countdown Latch
A CountDownLatch delays the progress of threads until the Latch reaches its terminal state. The main usage for a CoundDownLatch is to ensure that an activity does not proceed until another one-time action has been completed.
public class CountDownLatch {
public CountDownLatch(int count) {...}
// Causes the current thread to wait until the latch has counted down to zero
public void await() {...}
// Decrements the count, releasing all waiting threads if the count reaches zero.
public void countDown() {...}
public long getCount() {...}
}
Here there are two common scenarios. Either a thread wants to wait until some other actions are done, or a thread is used a sort of starting gun for other threads.
class KeyValueStore {
private final Map<String, Object> m = new TreeMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Object get(String key) {
r.lock(); try { return m.get(key); } finally { r.unlock(); }
}
public Set<String> allKeys() {
r.lock(); try { return new HashSet<>(m.keySet()); } finally { r.unlock(); }
}
public void put(String key, Object value) {
w.lock(); try { m.put(key, value); } finally { w.unlock(); }
}
public void clear() {
w.lock(); try { m.clear(); } finally { w.unlock(); }
}
}
Cyclic Barrier
A CyclicBarrier allows a set of threads to all wait for each other to reach a common barrier point.
public class CyclicBarrier {
public CyclicBarrier(int nThreads) {...}
public CyclicBarrier(int nThreads, Runnable barrierAction)
public void await() {...}
}
Exchanger
An Exchanger allows two threads to wait for each other and exchange an object. This can be especially useful when the object is very big as it can be reused.
public class Exchanger<T> {
public T exchange(T t) {...}
}
Blocking Queue
A BlockingQueue is a queue that supports operations to wait for the queue to become non-empty when retrieving an element, and wait for space to become available when storing an element. This is especially commonly used in the Product-Consumer pattern.
public interface BlockingQueue<E> extends Queue<E> {
E take() throws InterruptedException;
void put(E e) throws InterruptedException;
...
}