paint-brush
Thread Communication in Java using Lock and Condition; A Tutorial by@datmt
703 reads
703 reads

Thread Communication in Java using Lock and Condition; A Tutorial

by ĐạtSeptember 8th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Java provides mechanisms for threads to communicate with each other when working on common resources. This post gives you a concrete example of threads communication. Let's see how we can use this scenario to demonstrate thread communication. For this example, let's create two Runnable tasks: One to simulate Alice's paycheck and the other to simulate her purchase. We also need a class to represent her bank account class: BankAccount. The most interesting details are the lock and a condition at the beginning of the class.
featured image - Thread Communication in Java using Lock and Condition; A Tutorial
Đạt HackerNoon profile picture

Overview

Java provides mechanisms for threads to communicate with each other when working on common 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 also has a love for 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 use this scenario to demonstrate thread communication.

Code implementation

For this scenario, let's create two Runnable tasks: One to simulate Alice's paycheck and the 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:

Thread communication in action 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.


Different purchasing order 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 :( )

Conclusion

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.



Also published here.