Custom synchronization in Java using intrinsic locks, wait() and notify(). Example with a custom Bounded BlockingQueue
In this article, an example of a bounded BlockingQueue implemented with native wait()
,notify()
and synchronized
blocks, as well as the explanations of each method step by step in case you are not familiar with them.
wait() and notify(): what they do and how to use them
wait()
basically blocks the execution (of the current thread, obviously) until the correspondent notify()
is called on the same monitor.
In practical terms, this happens inside a synchronized method (where the monitor is the class so you can only have one monitor) or a synchronized block where you decide which monitor to use.
wait() and notify() using synchronized methods
Below I’m presenting an example where I call the synchronized
method that waits and after half a second I call the notify()
so that the execution can continue.
public class WaitNotifyExampleSyncMethods {
synchronized void unlockAndContinue() {
notify(); // <-- notify !!
}
synchronized void lockUntilNotified() throws InterruptedException {
System.out.println("Waiting...");
wait(); // <-- wait !!
System.out.println("Notified. I can now continue");
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyExampleSyncMethods waitNotifyExample = new WaitNotifyExampleSyncMethods();
new Thread(() -> {
try {
Thread.sleep(500);
waitNotifyExample.unlockAndContinue(); // <-- notify, unlocking
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
waitNotifyExample.lockUntilNotified(); // <-- wait, locking
System.out.println("End of main method");
}
}
The output is the following
Waiting...
Notified. I can now continue
End of main method
wait() and notify() using synchronized blocks
In case I want to use a custom object as a monitor (normally preferred in case you want more than one on the same class), this is the equivalent code producing the same output. Note that the monitor is a simple java Object
and that I call wait() and notify() on this object, from synchronized blocks also using the same monitor/object.
public class WaitNotifyExampleCustomMonitor {
public static void main(String[] args) throws InterruptedException {
Object monitor = new Object();
new Thread(() -> {
try {
Thread.sleep(500);
synchronized (monitor) {
monitor.notify(); // <-- (1)
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
synchronized (monitor) {
System.out.println("Waiting...");
monitor.wait(); // <-- (2)
System.out.println("Notified. I can now continue");
}
System.out.println("End of main method");
}
}
BlockingQueue (in case you don’t know)
A BlockingQueue is exactly what we need to implement a producer/consumer on the same machine.
With their put()
and take()
methods, it basically automatically blocks the execution respectively until there is space in the queue and elements are available. You can find many code examples online, like here.
Implementing a bounded BlockingQueue using wait() and notify()
Below I’m pasting a minimal example of some code that has a plain Java LinkedList
holding elements and two threads that I launch and wait for them to finish in the main method:
- The producer thread puts 1000 elements in the list. Note that if the queue is full I call
wait()
until that’s still the case, and continue only otherwise. After the element is added, I callnotify()
so that the consumer thread (potentially waiting as there were no elements) know knows there is something to consume. - The consumer thread works in a daemon mode under a
while(true)
and stops in case it receives a poison pill (that simply is the last element). Specularly to above, if the queue is empty it callswait()
until that’s still the case. After the element is consumed, I callnotify()
so that the producer thread (potentially waiting as the queue is full) knows there is now a free place to add at least one element.
import java.util.LinkedList;
import java.util.Random;
public class ProducerConsumerCustomSync {
LinkedList<Integer> queue = new LinkedList<>();
static final int LIMIT = 10;
Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
ProducerConsumerCustomSync pc = new ProducerConsumerCustomSync();
Thread producerThread = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread consumerThread = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
producerThread.start();
consumerThread.start();
producerThread.join();
consumerThread.join();
System.out.println("end");
}
public void produce() throws InterruptedException {
int i = 0;
while (i < 1000) {
synchronized (lock) {
while (queue.size() == LIMIT) {
System.out.println("queue is full. wait for consumer to pop...");
lock.wait();
}
queue.add(i++);
if (i%100 == 0) {
Thread.sleep(new Random().nextInt(200));
}
lock.notify(); // unblock consumer in case it waits for elements
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (lock) {
while (queue.size() == 0) {
System.out.println("queue is empty. wait for producer to add items...");
lock.wait();
}
Integer pop = queue.removeFirst();
lock.notify(); // unlock producer in case it was full
System.out.printf("Taken %d from queue\n", pop);
if (pop == 999) {
System.out.printf("poison pill received !");
return;
}
}
}
}
}
Launching the above produces the following
queue is full. wait for consumer to pop...
Taken 0 from queue
...
Taken 9 from queue
queue is empty. wait for producer to add items...
queue is full. wait for consumer to pop...
Taken 10 from queue
...
Taken 18 from queue
Taken 19 from queue
queue is empty. wait for producer to add items...
queue is full. wait for consumer to pop...
Taken 20 from queue
...
Taken 985 from queue
Taken 986 from queue
queue is empty. wait for producer to add items...
queue is full. wait for consumer to pop...
Taken 987 from queue
Taken 988 from queue
...
Taken 996 from queue
queue is empty. wait for producer to add items...
Taken 997 from queue
Taken 998 from queue
Taken 999 from queue
poison pill received !end
Process finished with exit code 0
Clap if useful, follow for more