DEV Community

Mạnh Đạt
Mạnh Đạt

Posted on • Originally published at datmt.com

Java Concurrency: Threads communication tutorial

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 convert this scenario to demonstrate thread communication.

Code implementation

For this scenario, let's create two Runnable tasks: One to simulate Alice 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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 signalAllon 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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, 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 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

This post is a part of the Java concurrency tutorial series

Top comments (0)