Table of Contents
Overview
Java provides mechanisms for threads to communicate with each other when working on shared resources. This post gives you a concrete example of threads communication.
Threads communication example
Let’s consider this scenario. Alice is a computer programmer who loves buying new shiny things (phones, laptops, gadgets). She has a list of things to buy. She works at a company called MonkeyTypes Inc. Her paycheck is $1,000 monthly.
Imagine she currently has this wishlist:
- A new Macbook: $3000
- A new mechanical keyboard: $400,
- A new phone: $500,
- A new shiny gadget: $500
She wants to have all of them in no particular order.
Her current balance is $0. What she wants is when she gets her monthly salary, she would spend the money in her balance to buy any of those things.
Let’s see how we can convert this scenario to demonstrate thread communication.
Code implementation
For this scenario, let’s create two Runnable tasks: One to simulate Alice’s paycheck and other to simulate her purchase.
We also need a class to represent her bank account.
Let’s create the bank account class first:
class BankAccount { private int balance = 0; private static Lock lock = new ReentrantLock(); private static Condition paycheckArrivedCondition = lock.newCondition(); public void getPaid(int amount) { lock.lock(); try { System.out.println("Getting paid " + amount); balance += amount; paycheckArrivedCondition.signalAll(); } finally { lock.unlock(); } } public void withdraw(int amount, String purpose) { lock.lock(); try { while (balance < amount) { paycheckArrivedCondition.await(); } System.out.println("Withdraw " + amount + " to " + purpose); balance -= amount; System.out.println("new balance -> " + balance); } catch (InterruptedException ex) { ex.printStackTrace(); } finally { lock.unlock(); } } }
As you can see, this class has one field: balance
to hold the current balance. Also, there are two methods to deposit and withdraw money to and from the balance.
The most interesting details here are the lock and the condition. I created a static lock and a static condition at the beginning of the class. The lock, as you may know, helps synchronize access to the balance. The condition, on the other hand, helps make communication between threads possible.
The withdraw() method
At the beginning of the withdraw
method, the lock
method on the ReentrantLock instance is called. This ensures that only the thread that has the lock can execute the code in this function.
Next, the try/catch/finally blocks make sure the lock is released at the end.
The while loop checks if the balance has enough money, if not, the await
function is called on the condition instance. This call releases the lock.
The deposit() method
Similar to the withdraw()
method, threads need to acquire the lock to execute code here. One interesting thing about this method is the call to the method signalAll
on the condition instance. This call is the meat of thread communication. This wakes up all the waiting threads and the check for balance > amount starts again.
The Runnable class to deposit money
Now the BankAccount class is available, let’s create a runnable class to deposit money to a BankAccount instance:
class PayEmployee implements Runnable { private final BankAccount bankAccount; private final int amount; PayEmployee(BankAccount employeeBankAccount, int amount) { this.bankAccount = employeeBankAccount; this.amount = amount; } @Override public void run() { bankAccount.getPaid(amount); } }
The Runnable class to withdraw money
class BuyThings implements Runnable { private final BankAccount bankAccount; private final String purpose; private final int amount; public BuyThings(BankAccount account, String purpose, int amount) { this.bankAccount = account; this.purpose = purpose; this.amount = amount; System.out.println("Plan to " + purpose + " with " + amount); } @Override public void run() { bankAccount.withdraw(amount, purpose); } }
Alice buys things in action
Now let’s implement the code where Alice submits her wishlist.
public static void main(String[] args) { BankAccount myAccount = new BankAccount(); var executors = Executors.newFixedThreadPool(5); executors.submit(new BuyThings(myAccount, "buy new macbook pro", 3_000)); executors.submit(new BuyThings(myAccount, "buy new phone", 500)); executors.submit(new BuyThings(myAccount, "buy new keyboard", 400)); executors.submit(new BuyThings(myAccount, "buy new gadgets", 500)); int cycle = 6; while (cycle > 0) { try { Thread.sleep(5000); } catch (InterruptedException ex) { ex.printStackTrace(); } executors.submit(new PayEmployee(myAccount, 1_000)); cycle--; } executors.shutdown(); }
From line 4 to line 7, all her purchases are submitted.
From line 9 to line 19, I simulate her payment. Let’s say her contract with the company has only 6 months left. Let’s run the program and see the output:
As you can see, after getting paid the first $1000, Alice buys a phone and then a new keyboard … However, this order is not consistent. The next run may produce different order. One certain thing is a MacBook always gets purchased last because only after the next-to-final payment, Alice have enough money to afford this.
You may ask, what if Alice only has 4 payment cycles left instead of 6? That means she never has enough money for a MacBook. In such a case, the program will run forever because the buy Macbook thread keeps waiting for the condition to meet. (so sad 🙁 )
Thread communication using monitors
I’ve introduced you to the concept of thread communications using Lock and Condition. However, these classes are only available from java 5. Before that, monitors are used.
What are monitors? Monitors are regular objects that have the ability of mutual exclusion and synchronization. Any object can be a monitor.
Back to the example above, instead of calling await
and signalAll
, I can create an object, use it as a monitor, and call wait
, notifyAll
to achieve the same result.
Here is the example above rewritten using monitors:
class BankAccount { private int balance = 0; private static final Object monitor = new Object(); public void getPaid(int amount) { synchronized (monitor) { System.out.println("Getting paid " + amount); balance += amount; monitor.notifyAll(); } } public void withdraw(int amount, String purpose) { synchronized (monitor) { try { while (balance < amount) { monitor.wait(); } System.out.println("Withdraw " + amount + " to " + purpose); balance -= amount; System.out.println("new balance -> " + balance); } catch (InterruptedException ex) { ex.printStackTrace(); } } } }
The only difference here is instead of using Lock, I created an object and use it to synchronize access to the balance. When the balance is not enough, I called monitor.wait()
to all synchronization claims (similar to lock). When there is a new deposit, I called monitor.notifyAll
to wake all waiting threads.
One important thing here is monitor
is a static instance, similar to lock and condition in the first example. Without being static, I will end up with multiple locks and monitors, which makes the code not work as intended.
Thread interruption & daemon threads
Conclusion
In this post, I have introduced you to the concept of thread communication in Java using Lock and condition. A thread can acquire the lock, and check if the condition is met. If the condition is not met, a call to await
on the condition instance releases the lock for other threads. A thread can notify all other threads by using signalAll
(or signal
to notify a random thread).
The code for this post is available here on Github
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.