Parallel programming in Java (1/4): In the kitchen (Threading)

Intro

This article is depending on the course (https://www.linkedin.com/learning/parallel-and-concurrent-programming-with-java-1) on linkedin learning

This is part 1 out of 4, to scratch the surface of parallel programming in Java, and how we can make use of threads using Java 11+

In each article, we will have a real-life example to align with the concepts, then we will elaborate on the code.

In the kitchen

Today we are in the kitchen. Chef (A) and Chef (B) are preparing Salad for us. let's see what will they face

Threads VS Process

The process is the combination of actions, info and data needed for a specific action, also it includes the threads, where every thread is responsible for a specific task

So, our kitchen will become the process, where we need to make a salad, Chef(A) will make a thread for chopping cucumber, Chef(B) will make a thread for slicing the onion, we might need another thread so we can include Chef(C) to make the dressing. All of those three chefs are included in the same kitchen and have access to the same resources, the same cookbook and the same vegetables.

We can have another kitchen that runs on making Pizza and another kitchen runs on making a cake.

Concurrency VS Parallel

"WE HAVE ONLY ONE KNIFE", said Chef(A)

Chef(B) smiled and said, "No problem we can work concurrently"

Chef(B) started slicing the onion, then gave the knife to Chef(A)

Chef(A) started chopping some cucumber, then gave it back to Chef(B)

Although it's not the practice, that's how concurrency deals with multiple things at once. We can do it better actually if we get another knife to work with

Now we have two knives, so we can work in Parallel

Thread lifecycle

  1. Chef(B) is called by Chef(A), and now Chef(B) is ready for any orders. standing still and waiting [NEW]

  2. Chef(B) is told Chef(A), I want you to prepare the dressing for the salad. Chef(B) said "Ok, understood. I'll be waiting until you tell me to start"

  3. Chef(B) is told by Chef(A) to start [RUNNABLE]

  4. Chef(B) found out that the mayo for the dressing is frozen, so we need to wait 30 minutes for the microwave. [TIMED_WAITING]

  5. Mayo is ready, and Chef(B) is completing the dressing now [RUNNABLE]

  6. Chef(A) is waiting for the dressing

  7. Chef(B) is now done, and leaving the kitchen [TERMINATED]

Scheduling execution

We have a lot of guests invited for tonight, so we hired more chefs. However we are still having only two knives, so we brought an organizer [Scheduler] to look over those two knives and choose who shall use them.

  • If one chef ends, the organizer takes the knife and passes it to another ready chef to complete his work

  • If one chef is pending on another task, then that chef shall give the turn to another chef

  • If one chef is taking too much time on the job, the organizer swaps that chef with another one to allow the others to complete their job as well [Context switch]

Note that the swap thing is taking time and effort because the organizer shall remember every single detail about the swapped chef, where is the stop point, and how the process shall continue.

Mainly this scheduling thing guarantees that everything is done with maximum fairness, throughput and minimum wait time and latency. However, each company has its regulations and roles regarding how to implement that

Daemon threads

The party is finally over, the chefs are ready to leave the kitchen to close it. However, one of them is still collecting the garbage, so they waited and waited and waited keeping the kitchen full. The best solution for this is to leave the kitchen and close it, and the last chef can keep on collecting things detached from them

In the code

If you start learning Java SE, you will be hit with (Threads) and (Runnable) interface, and (sleep) method. And you have the same questions and answers in each interview. But in these four articles, we are diving deeper than that to see how we can make the best use of those mysterious threads

Threads VS Process

"Process" is the parent or the container, it is an instance of the program, it includes the code, the data and state information. Each process is independent and has its own address space in the memory. You can have hundreds of active processes, and the operating system is responsible for handling them. Not to mention also it contains "Threads"

Threads are the basic unit for the operating system to manage, they are simply an independent path of executions. a bunch of code lines that run and have the same access to the shared code and shared data

Threads are "lightweight" compared to the process, and the operating system can switch between different threads faster than processes

Concurrency VS Parallel

"Can the computer run two operations in parallel?" The answer is simple. If it has more than one core, then yes it can.

But what if it has only one? it may seem like it's parallel, but it's not.

The processor is detaching the detachable tasks and keeps switching between them so that you might think it's working in parallel. But they actually working concurrently.

Creating thread in Java

There are three ways to create a thread in Java, you can use whatever suits you.



class ThreadOne extends Thread {
    public void run() {
        for (int i = 0 ; i < 10_000; i++)
            System.out.println("Thread 1");
    }
}

class ThreadTwo implements Runnable {
    public void run() {
        for (int i = 0 ; i < 10_000; i++)
            System.out.println("Thread 2");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread threadOne = new ThreadOne();
        Thread threadTwo = new Thread(new ThreadTwo());
        // Third way in case of anonymous threads (Or single use threads)
        Thread threadThree = new Thread(() -> {
            for (int i = 0 ; i < 10_000; i++)
                System.out.println("Thread 3");
        });

        threadOne.start();
        threadTwo.start();
        threadThree.start();

      }
}

But remember, as per best practice, it's better to implement an interface instead of extending an implemented class

Thread lifecycle

In Java, we have 6 states for the thread. At any point in time, you can check the status of the thread using this method (instance.getState())

  1. NEW -> The thread has been instantiated and is ready to run

  2. RUNNABLE -> The thread is executed in JVM (Ready for OS scheduler)

  3. BLOCKED -> The thread is blocked, waiting for a lock (Will talk about locks later)

  4. WAITING -> The thread indefinitely for another thread to perform an action

  5. TIMED_WAITING -> The thread is waiting for a definite time

  6. TERMINATED -> The thread has exited

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println(thread.getState()); // NEW

        thread.start();
        System.out.println(thread.getState()); // RUNNABLE

        Thread.sleep(500);
        System.out.println(thread.getState()); // TIMED_WAITING Waiting for the main thread for 500 ms

        thread.join();
        System.out.println(thread.getState()); // TERMINATED
    }
}

Thread attributes

Used attributes

Thread.sleep(1000); // Sleeps the thread for 1000 milliseconds, shall be surrounded by try-catch block
int count = Thread.getActiveCount(); // Returns the number of active threads running now
Thread instance = Thread.currentThread(); // Returns the current thread that's running
long id = instance.threadId() // To get the ID of the thread (getId) is deprecated later
instance.join() // Make the original thread wait until the instance thread terminates to continute the flow
Thread.State state = instance.getState() // Returns enum of the sattes mentioned earlier
instance.setDaemon(true) // Make the thread daemon (Explained later)
instance.setPriority(1) // Takes integer between 1-10, where 1 is the highest and 10 is the lowest

Run VS Start

The major difference is that, start is creating a new thread and running the (run) method inside it, while run is running the (run) method inside the current thread (Doesn't start a new thread)

Thread thread = new Thread(new Runnable(() -> System.out.println("HI")));
Thread.getActiveCount();
thread.run()
Thread.getActiveCount();
// The getActiveCount will return the same number
thread.start()
Thread.getActiveCount();
// The getActiveCount number will increase as a new thread has joined

Another major difference, you can call the (run) method multiple times. However you can call (start) only once, or it will through an exception

Scheduling execution

You can never count on the sequential of the threads, threads in runnable states are chosen by the scheduler of the operating system to run. So, they don't have to follow specific order or a specific time of implementation

If you run this code you will get different results each time for each thread


class ChopperThread extends Thread {
    int total_count = 0;
    static boolean chopping = true;

    ChopperThread(String name) {this.setName(name);}

    @Override
    public void run() {
        while (chopping) {
            System.out.println(this.getName() + ": " + total_count);
            total_count++;
        }

    }
}


public class Main {
    public static void main(String[] args) throws InterruptedException {
        ChopperThread chopperOne = new ChopperThread("One");
        ChopperThread chopperTwo = new ChopperThread("Two");

        chopperOne.start();
        chopperTwo.start();

        Thread.sleep(1000);
        ChopperThread.chopping = false;

        chopperOne.join();
        chopperTwo.join();

        System.out.println("Chopper one total is: " + chopperOne.total_count);
        System.out.println("Chopper two total is: " + chopperTwo.total_count);
    }
}

Daemon threads

Daemon threads are those threads that need be run in the background, and shall not be terminated when the process is terminated.

Garbage collector is one of the most known daemon threads, as it's a thread running in the background clearing unused variables to clear the memory, it shall be detached from the process.

The daemon thread is terminated when all the other threads are terminated and not needed anymore, it's terminated by the JVM

Thread thread = new Thread(new Runnable(() -> System.out.println("HI")));
thread.setDaemon(true); // Now this thread will keep running

Conclusion

Threads are a very important part of Java programming, also it's very easy to deal with just you have to know a few basics and we will cover them thoroughly.

Java threads are more fun and interesting than dull normal courses, and we will dive more into that through the upcoming articles