Effective Java Thread Management: A Fun Washroom Example

Weberton Faria
Just Eat Takeaway-tech

--

My previous article explored the exciting world of Virtual threads in Java 21. Virtual threads are a powerful feature that simplifies concurrent programming, making it more efficient and less error-prone. However, before diving deep into advanced topics like Virtual threads, it’s essential to understand the basics of threading and how to manage shared resources effectively.

One common challenge when working with threads is ensuring that multiple threads can access shared resources without conflicts. This is where synchronization comes into play. To illustrate these fundamental concepts in a fun and relatable way, we’ll use a simple yet amusing example involving a shared washroom.

In this article, we’ll break down the basics of threading in Java using the washroom example. You’ll learn how to synchronize access to a shared resource, manage thread coordination, and avoid common pitfalls. Let’s dive in!

Understanding Threads with the Washroom Example

To understand how threading and synchronization work in Java, let’s consider a simple scenario: a shared washroom. In this scenario, we have three types of users:

  1. People with Quick Needs: These are the users who need to use the washroom for a short period.
  2. People with Longer Needs: These users take a bit more time in the washroom.
  3. The Cleaner: This user cleans the washroom, but only when it’s dirty.

Here are the rules for our washroom:

  • Only one person can use the washroom at a time. Whether it’s for quick needs or longer needs, the washroom can only accommodate one user at a time.
  • After each use, the washroom is marked as dirty.
  • The cleaner checks if the washroom is dirty. If it is, the cleaner will clean it. The cleaner can only clean the washroom when no one else is using it.

This example is a perfect analogy for understanding how to manage access to a shared resource (the washroom) using threads. We’ll use Java synchronization mechanisms to ensure that only one thread (user) can access the resource at a time and to coordinate the actions of different threads.

Writing the Code and Explanation

We’ll start by defining the Washroom class and then incrementally add methods to handle quick needs, long needs, and cleaning.

Setting Up the Washroom Class

First, let’s create the basic structure of the Washroom class:

public class Washroom {
private boolean dirty = false; // Indicates if the washroom is dirty
}

Handling Quick Needs

Next, let’s add a method to handle quick needs in the washroom. This method will:

  1. Check if the Washroom is Dirty: If the washroom is dirty, the thread will wait until it is cleaned.
  2. Use the Washroom: Once the washroom is available, the thread will simulate a quick need with a short sleep, mark it as dirty, and then finish.
  3. Notify Other Threads: After finishing, the thread will notify all waiting threads that the washroom has been used and is now dirty.
    // Method for quick needs
public void useWashroomQuick() throws InterruptedException {
String name = Thread.currentThread().getName();
System.out.println(name + " is in line.");

synchronized (this) { // Synchronize on the current instance
while (dirty) {
System.out.println(name + " - washroom is dirty. Waiting to use the washroom (quick need)");
wait();
}
System.out.println(name + " is using the washroom for quick need");
Thread.sleep(5000); // Simulate quick need
dirty = true; // Mark washroom as dirty
System.out.println(name + " finished using the washroom");
notifyAll(); // Notify all waiting threads
}
}

This method uses synchronization to ensure that only one thread can execute the critical section at a time. If the washroom is dirty, the thread waits (wait()) until it is notified that the washroom has been cleaned. Once the washroom is available, the thread marks it as dirty and simulates using it with Thread.sleep(5000). After using the washroom, the thread notifies all waiting threads (notifyAll()) that the washroom is now dirty.

The synchronized is used with this to lock the current instance of the Washroom object. This means that the lock is associated with the specific instance, ensuring that no other thread can enter any synchronized block of code on the same object at the same time. In this context, this acts like a mutex or key that only one thread can hold at a time, ensuring exclusive access to the critical section of code.

Handling Long Needs

Now, let’s add a method to handle long needs in the washroom. This method follows a similar pattern to the quick needs method but simulates a longer usage time.

    // Method for long needs
public void useWashroomLong() throws InterruptedException {
String name = Thread.currentThread().getName();
System.out.println(name + " is in line.");

synchronized (this) {
while (dirty) {
System.out.println(name + " - washroom is dirty. Waiting to use the washroom (long need)");
wait();
}
System.out.println(name + " is using the washroom for long need");
Thread.sleep(8000); // Simulate long need
dirty = true;
System.out.println(name + " finished using the washroom");
notifyAll();
}
}

Cleaning the Washroom

Finally, let’s add the method to clean the washroom. This method will check if it is dirty. If the washroom is not dirty, the thread will wait until it is used. Once the washroom is dirty, the thread will clean it and mark it as clean. After cleaning, the thread will notify all waiting threads that the washroom is now clean.

    // Method for cleaning the washroom
public void cleanWashroom() throws InterruptedException {
String name = Thread.currentThread().getName();
synchronized (this) {
System.out.println(name + " is checking if the washroom needs cleaning.");
if (!dirty) {
System.out.println(name + " - washroom is clean. Leaving the washroom.");
return;
}
System.out.println(name + " is cleaning the washroom");
Thread.sleep(5000); // Simulate cleaning
dirty = false;
System.out.println(name + " finished cleaning the washroom. Leave the washroom.");
notifyAll();
}
}

Creating Task Classes for Washroom Simulation

We will now create specific task classes for each type of washroom task. Creating specific task classes for each type of washroom task is a good design choice. It makes the code more modular, adheres to the Single Responsibility Principle, and ensures clear separation of concerns. This approach improves readability and maintainability.

Defining the WashroomQuickTask

public class WashroomQuickTask implements Runnable {

private final Washroom washroom;

public WashroomQuickTask(Washroom washroom) {
this.washroom = washroom;
}

@Override
public void run() {
try {
washroom.useWashroomQuick();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

Defining the WashroomLongTask

public class WashroomLongTask implements Runnable {

private final Washroom washroom;

public WashroomLongTask(Washroom washroom) {
this.washroom = washroom;
}

@Override
public void run() {
try {
washroom.useWashroomLong();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

Defining the WashroomCleanTask

The cleaner task is placed in a while(true) loop to ensure it continuously checks and cleans the washroom as long as the program is running. Without the loop, the cleaner would clean the washroom just once and exit, leaving other tasks potentially waiting indefinitely if the washroom gets dirty again. The while(true) loop keeps the cleaner active and responsive, ensuring that the washroom remains usable for all other tasks.

public class WashroomCleanTask implements Runnable {

private final Washroom washroom;

public WashroomCleanTask(Washroom washroom) {
this.washroom = washroom;
}

@Override
public void run() {
while (true) {
try {
washroom.cleanWashroom();
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

Running the Scenario

Now, let’s create the threads to simulate people using and cleaning the washroom:

public class WashroomSimulation {

public static void main(String[] args) throws Exception{
Washroom washroom = new Washroom();

Thread person1 = new Thread(new WashroomQuickTask(washroom), "Person 1");
Thread person2 = new Thread(new WashroomLongTask(washroom), "Person 2");
Thread cleaner = new Thread(new WashroomCleanTask(washroom), "Cleaner");
Thread person4 = new Thread(new WashroomQuickTask(washroom), "Person 4");
cleaner.setDaemon(true);

person1.start();
person2.start();
cleaner.start();
person4.start();
}
}

In this simulation, we set the cleaner thread as a daemon thread using cleaner.setDaemon(true). This ensures that the cleaner thread runs continuously in the background, performing its cleaning task as long as the program is running. By making it a daemon thread, we allow the JVM to exit when all non-daemon threads have finished, ensuring that the cleaner doesn't prevent the program from shutting down properly. This setup ensures the washroom is always available for use without any hanging threads waiting indefinitely.

Here is an example of the result you might see when running this code:

Cleaner is checking if the washroom needs cleaning.
Person 4 is in line.
Person 2 is in line.
Person 1 is in line.
Cleaner - washroom is clean. Leaving the washroom.
Person 1 is using the washroom for quick need
Person 1 finished using the washroom
Person 2 - washroom is dirty. Waiting to use the washroom (long need)
Person 4 - washroom is dirty. Waiting to use the washroom (quick need)
Cleaner is cleaning the washroom
Cleaner finished cleaning the washroom. Leave the washroom.
Person 2 is using the washroom for long need
Person 2 finished using the washroom
Cleaner is checking if the washroom needs cleaning.
Cleaner is cleaning the washroom
Cleaner finished cleaning the washroom. Leave the washroom.
Person 4 is using the washroom for quick need
Person 4 finished using the washroom

Conclusion

In this article, we’ve explored the basics of threading and synchronization in Java using a washroom example. We created specific task classes for quick needs, long needs, and cleaning, ensuring a modular and maintainable design. By using the synchronized keyword, we ensured that only one thread could access the critical section at a time, effectively managing our shared resource – the washroom. The synchronized block, along with the this reference, acted like a mutex, providing exclusive access to the washroom for one thread at a time.

We also made the cleaner task run continuously in a daemon thread to keep the washroom clean as long as the program runs, ensuring that the washroom is always available for use. This setup avoided potential issues with threads waiting indefinitely.

I encourage you to experiment with different approaches and scenarios to deepen your understanding of threading and synchronization in Java. Try adding more tasks, handling different types of resources, or exploring other synchronization mechanisms.

Happy coding!

Want to come work with us at Just Eat Takeaway.com? Check out our open roles.

--

--