visit
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.
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.
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.
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);
}
}
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);
}
}
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();
}
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, does 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 :( )
In this post, I have introduced you to 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.