Threads in Java: From Kitchen to Code by Tarunkumar Mulchandani on January 5, 2026 44 views

In our previous post(Multitasking in kitchen), we explored the basics of threading through the lens of mom’s multitasking how she prepares breakfast by juggling multiple tasks efficiently. In this post, we’ll shift our focus to actual Java programming constructs for creating threads. We’ll also continue to relate it back to mom’s kitchen, because her routine still makes for a surprisingly accurate analogy.

Preparing Tiffin as a Process

Let’s say the overall job is to prepare tiffin. This involves multiple subtasks:

  • Chopping vegetables
  • Boiling rice
  • Cooking Curry

Each task can be done independently so why not treat each as a separate thread?

public class PrepareTiffin {
    public static void main(String[] args) {
        // Assign tasks here
    }
}

By default, the main() method runs on the Main Thread. But we can delegate independent tasks to separate threads for better efficiency.

Creating a Thread – Extending Thread Class

Let’s start by assigning the chopping work to its own thread:

class ChoppingVegetables extends Thread {
    @Override
    public void run() {
        System.out.println("Started chopping vegetables...");
        System.out.println("Peeling potatoes...");
        System.out.println("Dicing carrots...");
        System.out.println("Chopping complete.");
    }
}

Here, the run() method contains the actual steps we want the thread to follow just like how mom mentally lists steps when starting a new task.

To execute it:

public class PrepareTiffin {
    public static void main(String[] args) {
        ChoppingVegetables choppingTask = new ChoppingVegetables();
        choppingTask.start();  // Starts a new thread and runs the task
    }
}

Calling start() creates a new thread at the OS level and invokes the run() method in that thread.

This is important: if you call run() directly, it will behave like a normal method call and run in the main thread, not in parallel.

Anatomy of a Java Thread

Under the hood, each Java thread is directly mapped to an OS-level thread. When you call start(), the JVM uses native code to create a new OS thread.

Every thread in Java has associated properties:

  • Thread name – useful for logging or debugging
  • Thread group – for grouping related threads
  • Priority – determines thread scheduling preference

You can access or modify these with simple methods:

Thread current = Thread.currentThread();
System.out.println("Name: " + current.getName());
System.out.println("Priority: " + current.getPriority());
System.out.println("Group: " + current.getThreadGroup().getName());

These details help when managing or debugging complex multi-threaded applications.

Creating a Thread – Implementing Runnable

If you don’t want to extend Thread (Java allows only one class to be extended you can read more about it here), you can use the Runnable interface:


class BoilingRice implements Runnable {
    @Override
    public void run() {
        System.out.println("Washing rice..."); // Put In a Container and wash
        System.out.println("Putting rice on stove..."); //Stove It
        System.out.println("Waiting for it to boil...");
        waitForRiceToBoil();
        System.out.println("Rice is ready.");
    }
}

And execute it like this:


public class PrepareTiffin {
    public static void main(String[] args) {
        Runnable riceTask = new BoilingRice();
        Thread thread = new Thread(riceTask);
        thread.start();  // Starts the rice-boiling thread
    }
}

You can also use a lambda expression for one-off tasks:


Thread curryTask = new Thread(() -> {
    System.out.println("Heating oil...");
    System.out.println("Adding spices...");
    
    waitForCurry();
    System.out.println("Curry ready.");
});
curryTask.start();

Now Our PrepareTiffin analogy would look like following

public class PrepareTiffin {
    public static void main(String[] args) {
        BoilingRice riceTask = new BoilingRice();
        CookingCurry curryTask = new CookingCurry();
        MakingChapati chapatiTask = new MakingChapati(); // Optional third task 

        riceTask.start();
        curryTask.start();
        chapatiTask.start();

        System.out.println("🥡 Main thread: Ready to pack tiffin (without waiting)...");
    }
}

If we run the above code , the main thread will move on and print “Ready to pack tiffin…” even if the rice or sabji isn’t ready yet. This behavior is similar to situations in programming where we don’t wait for background tasks (I/O, DB operations, etc.) to finish, and attempt to act on incomplete data or processes.

In a real kitchen, you wouldn’t start packing the tiffin before the rice is cooked or the sabji is ready. You’d wait until all essential dishes are complete. Similarly, in a multi-threaded Java program, the main thread sometimes needs to wait for other threads to finish before proceeding.

That’s exactly what the join() method is for.

đź”§ What is join()?

The join() method tells the current thread (like main) to pause and wait until the thread it’s called on has completed its execution.

Syntax:

threadInstance.join();

Behind the scenes, the main thread pauses until the threadInstance completes its run() method.

In Our Case:

Without join(), the main thread rushed ahead and printed “Ready to pack tiffin” before cooking was done. By using join(), we coordinate tasks to ensure the food is ready before packing.

Example:


try {
    riceTask.join();   // Waits until rice is boiled
    curryTask.join();  // Waits until curry is cooked
    chapatiTask.join(); // Waits until chapatis are done
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("All dishes are ready. Now packing the tiffin!");

This simulates basic synchronization between threads no complex locking or shared resources yet, just waiting until everything’s ready.

While join() is a simple and safe way to coordinate threads, it introduces the broader topic of thread synchronization which becomes essential when multiple threads start sharing resources or depending on each other.

We’ll explore that in upcoming blogs where we’ll talk about:

  • Shared resources and memory
  • Race conditions
  • Locks and synchronized blocks

For now, join() gives us a neat and reliable way to maintain order in parallel task execution especially when certain steps must not be skipped ahead.

What If:

One thing that you might be wondering that while working with Threads we override the run() method and while invoking we invoke start() method. What if we invoke run() method directly

instead of start() method. Doing it via the run()

In this post, we moved from concept to code learning how to create threads in Java using both Thread and Runnable, and how join() helps us coordinate them like timed kitchen tasks. With these tools, we can now run multiple operations in parallel and still maintain order.

In the next blog, we’ll explore what happens when threads start sharing resources and how to handle synchronization to prevent conflicts in a multi-threaded Java program. Please continue following this program

đź’°The Memory Vault: Where Your Code Keeps Its Variables

About Author

Tarunkumar Mulchandani

Solution Analyst

A curious software engineer who enjoys exploring bare-metal concepts and diving into Linux, Java, and JavaScript. Currently learning tools like Docker and Jenkins. Always eager for discussions related to tech, maths and cricket. Get in touch with me: LinkedIn: https://www.linkedin.com/in/tarun-mulchandani-9bbb2321a Medium:medium.com/@tarun18973 Github: https://github.com/tbm02