Multithreading and Concurrency in Java Tutorial

In this section, we will learn what the multithreading and concurrency is and how they work in Java.

What is Process?

A process is a running instance of a program with all the resources that an operating system allocates to that program.

You see, at first, a program (let’s say the Microsoft Word) is nothing but an algorithm written and compiled using one or more programming languages (For example, C++ and C#). Now, when you install that program on your computer, all that’s happening is that you get a copy of that program in your computer’s hard-disk. At this stage, the target program (the Microsoft Word in this case) is not a process! This is because the program is just stored in your hard-disk and is not running.

Now, the moment you run this program (like by double-clicking on the icon of the program), the Operating System will get a copy of that program and puts it in the memory (RAM) and lets the CPU to run its instructions. Now this copied version of the program that is in a running state is called a process.

You can think of a process as a unit of execution, or a unit of activity, or a path of execution, if you will. This is because, the bottom line of every program is just a bunch of instructions that are sent to the CPU and they will be run one at a time by the CPU (That’s why we call them a path of execution or a unit of activity).

Note: another name of a process is Task.

Operating System and CPUs

First of all, the Central Process Unit (AKA CPU) is a chip that is in charge of running the instructions of any program on a computer.

For example, if you have a statement like `i = 20 + 10;` in one of your Java programs, this statement gets executed by the CPU. Meaning, it’s the CPU that adds the values 20 and 10 and again, it’s the CPU that assigns the result to the memory space where the `i` variable is representing.

Now, an operating system is like the manager or the director of the CPU! An operating system (AKA OS) decides which process (a running program) can have the access to the CPU to run its instructions. This is especially important when it comes to a computer with one or two CPUs (AKA Core) and multiple processes (Running programs) that are competing with each other to get a CPU’s time.

Because, the fact is, a CPU can run the instructions of only one process at a time! So, if there are multiple processes and the number of available CPUs is at a premium, then it’s the OS that should manage and divide the CPU’s time among all the available processes so that each gets a chance of running its instructions.

An operating system itself is a program just like any other program you see out there (But perhaps an advanced type of program)! It has instructions and just like other programs; it gets access to the CPU for a small period of time. During this time, an OS directs and gives instructions to the CPU to run which running program (process) next, and for how long.

Basically, the CPU after running a process, gives the control back to the OS and during that time, the OS decides which process should get the CPU’s time next and for how long. (Note that an OS has a data structure that contains information about the number of running processes and their states, etc. So it knows which process was waiting for a long time, or which one has a higher priority, etc. and based on this data, it knows which process should get the CPU’s time and what instruction of that process should be run).

What is Multitasking?

Multitasking is the ability of an operating system to run multiple processes at the same time or at once (AKA, in parallel).

This is mainly possible if the target computer has multiple CPUs (AKA Cores, Processors) and an equal number of processes for them to run.

But, if a computer has only one CPU, there’s no way for an operating system to run multiple processes at the same time (In parallel basically)!

This is because a CPU can run the instructions of one process at a time! Now, if the operating system wants to run the instructions for another process (other than the one that is currently running), the operating system has to:

  • Stop the execution of the current process.
  • Save the state of the current process somewhere in the memory (RAM). (This is important because the next time the CPU gets back to the stopped process to continue to run its instructions, using the stored state, it will be able to figure out what the last executed instructions was and where it should continue to run the rest of instructions).
  • Restore the state of another process (if that process was previously running and it got stopped by the operating system) and now let the CPU to run its instructions right where it was left previously.
  • Note that if the next process just got running for the first time, the CPU will run its instructions from the beginning (Wherever that beginning part might be).

Now, this division of a CPU’s time between multiple processes happens so fast that we as humans get an impression as if all the processes are running at the same time.

Note: the allocation of a CPU’s time between multiple processes is known as Context-Switching.

Threads in Java

We mention that an OS switch context from one process to another so that each gets a time with CPU to run its instructions.

Now, switching between one process to another means saving the state of that running process, perhaps moving it to somewhere else of the memory, reloading another process and making it ready to take the CPU (doing some housekeeping in general). This switching between one process to another is expensive relative to the time that will be spent to store one process and prepare another.

Also, let’s say we have a single process and in that process there are multiple instructions that could be executed independently from the rest of instructions of the process. For example, let’s say the process A has 4 instructions:

  • Instruction 1
  • Instruction 2
  • Instruction 3
  • Instruction 4

Now the first two instructions are related to each other. Meaning the result of the `instruction 2` depends on the result of the `instruction 1`. Also, the `instruction 4` depends on the result of the `instruction 3`. But these two groups of instructions are independent from each other!

Now the question is, wouldn’t be faster if we could run the first and second instructions in one processor and the third and fourth instructions in another processor (CPU)?

That process could run a lot faster if we could do that, right?

Well, the good news is, we can do that, and this is where the threads come in!

Threads are basically units of execution that we can create inside processes.

Each process has at least one thread (which is known as the main thread). But we can add more threads to a process and pass a set of instructions to each thread to run.

Now the OS divides the CPU’s time between these threads and allows them to run simultaneously.

So if there’s a process with two groups of instructions, independent from each other, we can create two threads in that process and pass the first group of instructions to the first thread and the second group of instructions to the second thread. Here, the OS, instead of switching from one process to another, it will switch between one thread to another! Thus, a process’s instructions run at the same time if that process has multiple threads.

Be aware that threads in a process use the same resource that the owner process gets from the OS! For example, threads share the heap memory!

But, each thread has its own Stack and the Program Counter!

Note: the program counter is a value that is maintained by the CPU and it refers to the current instruction of the target thread that is running. For example, if a thread has 10 instructions and a CPU is currently running the third instruction, thus the value of the program counter is 3. Also, this value increases at the end to point to the next instruction of the target thread.

We mentioned that each thread in a process has its own stack. This means if you have a local variable in a method and two threads invoked that method, each will invoke a copy of the method in its stack and run its instructions independent from the other.

This means there will be two local variables (each thread will have its own local variable in its stack).

Note: there’s no-way for a thread to access the stack of another thread. Their only way of communicating is through the memory of the owner process that is shared between the threads (like the heap area).

Now here’s another benefit of using threads in programming:

Switching between one thread to another is less expensive compared to switching between one process to another! This is because a thread has less memory footprint and so it’s a lot faster to switch between one thread to another in a process.

What is multithreading in Java?

As mentioned before, a thread is a unit of execution, a sub-process, or a sub-task in a process.

So, multithreading means having multiple threads or multiple units of execution (or path of execution in a process).

This is especially helpful if we have a running process with groups of instructions that could be run independently from each other! So by creating multiple threads in a process and allocating each group of instructions to each thread, the target process gets a chance of running its instructions at the same time which is potentially faster compared to sequentially running those instructions using only one thread.

What is Java Concurrency?

Concurrency means having multiple tasks (running processes or threads) and divide the time of a processor between them so that all of them get a chance of running their instructions by that processor. For example, if you have 2 running processes, the OS will allocate the CPU’s time to the first process, run a few instructions of it and then save the state of that process and move to the second one to execute its instructions. This switching process happens so fast that we as humans feel as if they are running at the same time.

Note that if the target computer has multiple cores (processors), there’s a chance that the OS may run each task or threads in a different processor and so truly run them in parallel.

But, as you’ll soon learn, parallelism only happens when we have multiple cores (processors), while concurrency can happen even when we have a single core or processor.

Context Switching and Its Connection with Multithreading

As mentioned before, context switching happens when there’s a limit on the number of available processors and there are multiple processes that want to have their instructions to be executed.

So here, the OS will divide the time of a CPU between all the available processes in order to create an impression, as if all the processes are running at the same time.

Now, in modern computers and operating systems, context switching is not limited to just processes but to their threads!

This means, if a process has multiple threads, the OS is able to switch between its threads and run those threads concurrently.

What’s the difference between multithreading and multitasking?

Multitasking happens when there are multiple processes and they want to take the time of a CPU to run their instructions. So if possible and there are multiple processors (cores) available, the OS can allocate each process (task) to a core so those processes can run at the same time. Of course, if there’s a limit on the number of available CPUs, the OS has to divide the time of those processors for each process and switch between them (context-switching).

On the other hand, multithreading happens on the threads of processes! For example, if there are two processes with 2 threads each, an OS can run one thread of each of these processes and then switch to other threads of the processes after a period without completely stopping one process over another one.

Parallel programming in Java

Parallel programming means running multiple tasks (threads of instructions or processes) on different cores (CPUs) literally at the same time.

This is only possible if the target computer has more than one CPU (core). In that case, the OS is smart enough to run a task in one core and another one in another core.

What’s the difference between concurrency and parallel programming in Java?

  • Parallelism in programming means `literally` running multiple tasks (like multiple threads) on different cores (CPU) so that they run at THE SAME TIME! Note that parallelism is not possible if the target computer has only one CPU (core) and there are multiple tasks to be run on that core.
  • On the other hand, concurrency means running multiple tasks at the same time, but not necessarily in a parallel model! Basically, a CPU constantly switches between multiple threads or tasks and thus gives the impression as if those tasks are running in parallel! Note that depending on the number of available cores in a computer, an OS may turn a concurrent operation into a parallel one! Meaning, the OS may allocate a CPU to one task and another CPU to another task, thus running them in parallel.

Basically, while we can have concurrency when there’s only one core in a computer, parallelism is not possible if there’s only one processor!

How to create a thread in Java?

There are multiple ways in which we can create a thread in Java. These methods of creating threads is covered in the rest of this section as well as the next section (Creating and starting threads in Java section). But to start with, one way to create threads in Java is by creating an instance of a class called `Thread`.

For example, if you create an instance of this class like:

Thread t1 = new Thread();

Now you have an object in your program that represents a thread and could give it instructions to run concurrently.

Notes:

  • When creating an instance of the `Thread` class, the JVM won’t automatically create a thread and start to run it! All that is happening is that we’re creating an object and it is stored in the heap area of the memory.
  • In order to actually run this thread and make the JVM to allocate the necessary resources to it, there’s a method called `start()` that needs to be called over the created instance.

Alright, before we get into more details, first let’s run an example, after that, we will continue our discussions on how to build threads in a Java program.

Example: Creating multithreaded program in Java

public class Test {

    public static void main(String[] args) {
        Thread t1 = new Thread();
        t1.start();
    }

}

How multithreading works in Java?

In this example, we’ve created an instance from the `Thread` class and stored the address of that object into the t1 variable. Remember that only an object is created here. There’s no new thread in this program yet!

Note: every Java programs have at least one thread by default, which is known as the Main thread. So when our program ran, JVM automatically gave it one thread and used that thread to run the instructions of the program.

Now when it comes to the second statement of this program:

t1.start();

This is where the magic is happening! This statement signals the JVM that this program needs another thread (besides the main thread that is currently running). So the JVM will create a new thread, allocate the required resource for that new thread (Like a new stack area) and then run the instructions that are allocated to this new thread.

But wait a second! Where’s the instructions for this new thread? Well, we didn’t allocate anything for this new thread to run!

So what happened here is that the new thread got the resource from the JVM, but because there weren’t any instructions for it to run, we didn’t see anything after running this program.

Java Thread.start() method

Just to remind you one more time, when creating an instance in Java using the `Thread` class, you’re just creating an object of this type! There won’t be any actual thread in your program as long as you don’t call the `start()` method of this object.

Now, after you’ve called the start() method, then the JVM will create the actual thread for your program and start to run the instructions set for this new thread.

Java Runnable interface

So far, we’ve seen how to create a thread in Java, but one thing that we still don’t know is the fact of how to allocate instructions for a thread to run?!

An object of type Thread has a method called `run()`. This is the place that we can put the instructions so that when a thread starts to run, the instructions within the body of this method will be run as well.

By default, if you create an object of type Thread, the run() method will have an empty body and so after calling the start() method, you won’t see any action to happen from that thread.

But, this run() method is not originally created in the `Thread` class! It’s coming from a functional interface called `Runnable`.

You see, the Thread class has multiple constructors. One of them that we’ve seen so far takes no argument (which is known as the default one). Now, if we create an instance using this constructor, the thread will use the `run()` method of the Thread class (the one that has an empty body) and so we won’t see any actual task to run at the end.

But, the Thread class also has another constructor that has one parameter of type `Runnable` interface. Now, if we call this constructor and pass an instance of the Runnable interface as its argument, then the created thread will use the `run()` method of the Runnable instance (the one that we’ve passed as the argument of the constructor) and runs the instructions of that method instead.

This means we can create an instance of the functional interface Runnable, and define any instructions we want in the body of the run() method. At the end, the target thread will execute the body of this method.

Notes:

  • A thread only runs the body of the run() method and after that, the work of the thread is done and basically we say the thread is dead! Meaning you can’t call the start() method of the same thread two times in a row! (Note: you can think of the run() method as the main() method of the threads that we create using the Thread class. Just like the Main thread that after running the entire instructions of the main() method, its work will be done and essentially the program will close, for those threads that we create using the Thread class, after running the run() method, the work of those threads end and they will be gone afterward.)
  • Please check the Functional Interface section if you’re not familiar with this type of interface.

Example: using the Runnable Interface in Java

public class Test {

    public static void main(String[] args) {
        
        Runnable runnable = ()->{
          for (int i = 0 ; i<5; i++){
              System.out.println(Thread.currentThread().getName());
          }
        };
        Thread t1 = new Thread(runnable, "Thread One");
        Thread t2 = new Thread(runnable, "Thread Two");
        t1.start();
        t2.start();
    }

}

Output:

Thread One

Thread One

Thread Two

Thread Two

Thread Two

Thread Two

Thread Two

Thread One

Thread One

Thread One

How does the Runnable Interface Work in Java?

In the last example, we have an instance of the functional interface Runnable and assigned its address to the runnable variable. Note that for its `run()` method, we’ve set a loop that executes 5 times and per each iteration, it will send the name of the thread that is executing the body of this method. (We will talk about the currentThread() and getName() methods in later sections).

Now, this Runnable instance can be passed to a Thread instance and the body of the run() method will be executed by that thread at runtime as a result.

After that, we’ve created two instances from the Thread class and used one of its constructors that takes an instance of the Runnable interface as its argument. (Note: a thread can also have a name and we can set that name by passing a string value as the second argument of the constructor. This name is especially helpful when debugging a multithreaded program).

Now, at runtime, after the execution of the two calls to the `start()` method, two threads will be created and each will take a copy of the `run()` method and they will start to run the instructions of this method independently.

As you can see from the output, because each thread runs concurrently, the result is not sequential anymore! Meaning, first, the first thread got a chance of connecting to the CPU and two of its instructions ran immediately, after that, the second thread got the CPU’s time and ran its entire instructions, and after that, another context switch happened and the first thread again got the CPU’s time and ran the rest of its instructions. (Because threads are running independently, every time you run this program, you’ll get a different result).

Note that each thread has its own stack and, for example, in this little program, each thread took a copy of the instance of the run() method and brought it to its own stack and started to run the instructions in the method.

Advantage of Multithreading in Java

Perhaps the greatest advantage of using multiple threads in a program is the increase of speed that will be gained because thread runs a portion of instructions of the target program independently on different processors and so potentially it will take less time to execute the entire instructions of that program relative to if we wanted to use only one thread to execute the same program.

Note that I used the word `potentially`. This is because in some scenarios, using multithreading won’t help to increase the speed of your program! This is mainly because each time we create a new thread; it takes a bit of time for the OS to set aside the related resources for that thread. Also, switching between multiple threads of a process, although is not that expensive but still, it will take a bit of time!

So if there’s a program that does not have that much of CPU-intensive tasks, it might be better off going with a single thread instead of creating a multiple thread for that program! (At the end, it’s all about trial and error to see if a single thread is a better option for your program or multiple thread. If the multithreading is the way to go, still you need to run tests to see how many threads your program needs to get the best performance).

Another benefit of using multiple threads is when your program has one or more tasks that takes a long period of time to complete (Just to make it clear, in the world of computers and user-experience, even half a second could be translated as a long period of time)! For example, let’s say part of the work of your program is to send a request to an external resource and it needs to wait until it gets a response from that resource. So here if we used single thread, that could block the program for an unknown period of time and thus creating a bad user-experience! Now, using multithreading, we can create another thread and put that blocking instructions on this newly created thread, allowing the main thread to continue to run and serve other requests of users without making them to wait (Hence creating a better experience).

Leave a Reply