Understanding Threads & Multi-threading in Java
In Java, understanding multi-threading is very crucial and essential for every Java developer. Above all, Java is a multi-threaded programming language, which means java provides built-in support for multi-threading. So, that’s why I’m going to discuss about multi-threading in this article. But before moving into the multi-threading, we should get proper understanding about “Threads”.
Threads in Java
A thread is simply a flow of execution, and every Java program have at least one thread known as a main thread. The main thread is created by the JVM whenever you run a java program. Above all, threads help your Java program to do multiple tasks at the same time in a very effective way.
In Java, there are two types of threads, Daemon threads and Non-Daemon threads. Daemon threads are basically low priority threads that used to perform supporting tasks, while Non-Daemon threads are the high priority threads that designed to perform specific tasks.
In fact, JVM always waits until non-daemon threads to finish their work. It never exits until the last non-daemon thread finishes its work.
Creating a Thread in Java
In Java, we have two different ways to create a thread.
- By extending Thread class
- By implementing Runnable interface
01) Creating a thread using Thread class
In the following code snippet, you can see how to create a thread by extending the Thread class.
As you can see the “Printer” class is extending the Thread class in the the 3rd line. Then it overrides and implements the “run” method from Thread class. This is where you should implement the task that you want to execute in this thread. In this example it going to print from 0 to 200.
In order to create the thread, I have created an instance of the thread by instantiating the “Printer” class in the “main” method. Now all we have to do is call the “start” method using “printer” instance. The “start” method has code to invoke the “run” method and create a thread when it gets called. Now the thread will start executing. These are the basic steps to create a thread using Thread class.
02) Creating a thread using Runnable interface
In the following code snippet, you can see how to create a thread by implementing the Runnable interface.
As you can see its really similar to the process of creating thread using Thread class. But instead of extending Thread class, you have to implement Runnable interface from the “Printer” class. However, if you don’t override and implement the “run” method, it will give you a compilation error. Because if you implement an interface you must override and implement all the abstract methods of that interface.
In this scenario you can see I had to create an object of a Thread class and pass the Runnable instance (printer) for that. Then the “start” method called using the “thread” object. The reason for this, because Runnable interface doesn’t have a method call “start” and it only comes within the Thread class. So, in that case you have to follow these steps to create thread using Runnable interface.
Also, you need to know that the Thread class has multiple constructors as below,
So, now you know there are 2 different ways to create a thread in java. Then why there are 2 different ways and what are the differences among these? Actually, we cannot specify one is better and other one is not. Because no matter which way you use, it doesn’t affect to the behavior of thread. Still there are certain things that you need to know. The main scenario that you need to consider what option to use is, when inheriting (extending) classes in your program.
In your program you may extending classes to inherit methods and attributes from other classes. It may be basic inherit or multilevel inherit. As an example, consider the following image.
In here you can see “iPhone 11” class is extending “iPhone” class and that class extending “Apple” class. So, this is how “Multilevel Inheritance” work in java. But the thing is if you want to create thread for “iPhone 11” using Thread class, then you have to break the relationship with the “iPhone” class. Because Java doesn’t support for “Multiple Inheritance”. In that case we have to use Runnable interface instead of Thread class. In java we can achieve multiple inheritance through interfaces. So now you can create a thread using Runnable interface while extending the “iPhone” class.
Note: In such scenario you need to “extend” the class before “implementing” interface.
public class IPhone11 extends IPhone implements Runnable {}
Thread Lifecycle
There are several states in thread lifecycle.
- New: Whenever you create a thread, it will come to this state immediately.
- Runnable: In this state, thread is ready to execute.
- Running: When the thread starts executing, then the state is changed to “running” state. The scheduler selects one thread from the thread pool, and it starts executing in the application.
- Waiting: This is the state when a thread has to wait. Sometimes, a thread transitions to the waiting state while the thread waits for another thread to perform a task. A thread transitions back to the runnable state only when another thread signals the waiting thread to continue executing.
- Dead: A thread enters the “Dead” state when it completes its task.
Multi-threading in Java
In the above chapter, we have covered most of the knowledge that necessary to continue the learning in multi-threading. As you already know, Java is a multi-threaded programming language which means java provides built-in support for multi-threading. So, let’s see how we can create multiple threads and help our Java program to do multiple tasks (multitasking) at the same time in very effective way.
Multitasking can be divided into 2 categories based on its behavior.
- Process-Based multitasking: When the operation system runs multiple processes at the same time.
- Thread-Based multitasking: When two or more threads run concurrently, that belongs to a same process.
In the IT community there is common misunderstanding about the threads. That is, threads are there to cut-down the time. For an example assume there is process that has 5 steps that take the time up to 10 minutes. But the thing is you cannot increase threads to cut-down time up to 1–2 minutes. However, you can cut-down time up to some point (7–8 minutes), but still you cannot perform miracles using threads.
Another reason why you cannot use threads as you want is, there may be tasks that depend on other tasks. In a process there may be a task to perform on database, but before performing on database it may need some data from another task. So, in such case you must have proper knowledge to handle threads and decide what tasks should you add to a separate thread.
Let’s see how multi-threading work with an example.
So, now you can see the result of execution from this program. However, if we try to execute the program several time, all these executions will give us different results to each other. Also, in every execution you can notice, that threads are not executed in a same order. The main reason for this behavior is, there is no guarantee to create the thread instantly even though you call the “start” method. So, this is where thread scheduler comes. When you start the thread what it does is, if there is no error it will add to the thread scheduler. However, the behavior of this thread scheduler is completely depending on the instance of JVM. Because of that, there is no guarantee which order threads going to be executed.
In the following section you will see some scenarios, that may be going to useful when dealing with threads in java.
🔹What happen if we do not override the “run” method?
- To check, you can create a class and extend the Thread class.
- Then without overriding the “run” method, create an instance of thread in the Main class.
- Now call “start” method on that instance of thread. Now you will see the result.
The result:
It won’t do anything, not even an error. So, it’s not must to override the “run” method, but it would be useless approach without overriding “run” method
🔹What happen if we directly invoked the “run” method (without calling “start” method)?
- To check, you can create a class and extend the Thread class.
- Then override and implement the “run” method within that class, then create an instance of thread in the Main class.
- Now instead calling “start” method, call the “run” method on the instance of thread.
The result:
It will execute the “run” method without any error. But the thing is it will not create a thread as “start” method. So, the “start” method is required to create a thread and invoke the “run” method. However, creating thread is required several steps and the “start” method take care all these in behind. So “start” method have to check,
- Whether the thread is already exists
- Whether thread is ready to run
- Registering the thread on registers
- Adding the thread into the thread pool
🔹What happen if we override the “start” method?
- To check, you can create a class and extend the Thread class.
- Then override and implement the “start” method within that class, then create an instance of thread in the Main class.
- Now call “start” method on that instance of thread. Now you will see the result.
The result:
It will execute the “start” method without an error. But it won’t create a thread either. Because when you override a parent class method and give an own implementation, it always executes the override method on the child-class. But still you can achieve what you want even in this way by simply calling “start” method on Thread class, using “super” keyword.
Thread Priority
In java the order of threads’ executions cannot be predicted. But java provides a mechanism to assign priorities for the threads one over another. The maximum value we can assign as a priority is 10 and the lowest value is 1 while 5 is the middle value.
There is bit confusion between developers that 5 is the default priority value for every thread that we create, but it is not. Only the main method comes with a default value and its 5. So, what about other threads? These threads don’t have a default priority. However, when we create a thread it comes with a priority. That’s because these threads inherit the priority value from the parent thread. So, if we assign a value for main thread before creating another thread, all the other threads going to inherit that priority value as their priority value.
Result:
Main thread before: 5
Main thread after: 7
Child thread: 7
So, what happen if we assign a value more than 10 as a priority value?
- In that case it will throw an “IllegalArgument-Exception”, because you cannot assign more than 10 as a priority value.
What happen if we assign same priority value for two different threads?
- In that case you cannot predict what thread going to be run by the thread scheduler. However, you won’t identify any significant change either.
Thread.join() Method
In java we can use “join” method on thread, so the current thread goes into a waiting state until other thread to completes its task. This join method has 3 overloaded methods.
join(): So, this “join” method will keep waiting until the other thread completes its task. That means, it doesn’t matter how much time it takes, if we use this no-argument “join” method, that thread has to wait whole the time.
join(long milliseconds): Using this “join” method we can pass the time in milliseconds. If we use this “join” method and pass 10000 milliseconds (10 seconds), the current thread will only wait for the passed time. If the other thread cannot complete its task within the given time, current thread will not be going to wait and it goes back to the “runnable” state.
join(long milliseconds, int nanoseconds): This “join” method has the same behavior as the previous one but with more precise time.
Thread.yield() Method
This method provides a mechanism to inform the thread scheduler, that the current thread is willing to give its chance to other thread, but it would like to be scheduled back soon as possible. The “yield” is a native method in java, means it is not implemented within the java.
So, assume like there are several threads named as t1, t2, and t3. From these threads we going to call “yield” method on t1. After that t1 will go to the “waiting” state. But the thing is after t2 completes its tasks, there is no guarantee that t1 will be going to execute next. It might be t1 or t3.
Thread.sleep() Method
Using this method, we can pause the execution of current thread for specified time in milliseconds. So, the current thread goes into the “waiting” state. But it comes back when the waiting time is expired or if you interrupt it. The “sleep” method also has 2 overloaded methods.
sleep(long milliseconds): A native method. In this “sleep” method we can pass the time as milliseconds.
sleep(long milliseconds, int nanoseconds): A non-native method. This “sleep” method has the same behavior as the previous one but with more precise time.
So, this is the end of the article and I hope you enjoyed it. Happy Coding👨💻.
References
Threads in Java. 2018. [video] Directed by K. Dinesh. https://www.youtube.com/watch?v=Y9JDbm8edOk&list=PLD-mYtebG3X99o6vJ3uR5P6UcH3MQSWBH: YouTube.
SINGH, C., n.d. Multithreading in java with examples. [online] beginnersbook.com. Available at: <https://beginnersbook.com/2013/03/multithreading-in-java/> [Accessed 11 May 2021].
Guru99.com. n.d. Multithreading in Java Tutorial with Examples. [online] Available at: <https://www.guru99.com/multithreading-java.html#5> [Accessed 10 May 2021].
Tutorialspoint.com. n.d. Java.lang.Thread Class — Tutorialspoint. [online] Available at: <https://www.tutorialspoint.com/java/lang/java_lang_thread.htm> [Accessed 11 May 2021].