When we talk about Multi-Tasking, it s mainly divided into Process-Based multitasking and Thread-Based multitasking. Process-based multitasking allows your computer operating system to run two or more programs concurrently. For Example, You can listen to music on your media player and at the same time use applications like Text Editors. On the other hand, Thread-Based multitasking allows a single program to perform two or more tasks simultaneously. For example, the Text Editor checks the grammar at the same time we are formatting the text.
Therefore, we can say that Multithreading is a feature that allows concurrent execution of two or more parts of a single program for maximum utilization of CPU. Each part of such a program is called a thread. So, threads are lightweight processes within an operating system process. Some of the advantages of Multithreading are;
- Helps to build reactive systems
- Helps to build responsive user input GUI application without freezing screen during time-consuming tasks
- The server can handle multiple clients simultaneously
- Can take advantage of parallel processing
Just think you have a process that takes 10 min to complete. One of the main misconceptions most of us are having is that we can use around 10 threads and minimize the processing time of that task to 1 min.
That is a wrong concept because we can use threads to achieve concurrency only if the tasks inside that process are not dependent on each other. If all the tasks are dependent on each other or some of the tasks are dependent on each other, you can not use thread to achieve concurrency on those tasks.
1. How to Create Threads
Mainly there are two ways of implementing threads. One is you can extend your class with Thread class and override its run() method. Then you can create an object from that class and call the start() method. The other way is, you can implement the functional interface Runnable in your class and override its run() method. Then you can create an object from your runnable implemented class and pass that object to the constructor of the Thread class to create a thread object and call the start() method on created thread object. You can see the code for the above two ways below;
🔸 When we extend using the Thread class it is not a must to override the run() method, but nothing will happen when we call the start() method on the thread.
🔸 But if we use the implements Runnable interface, we have to override the run() method from that interface.
🔸 When we use the t1.run() instead of the t1.start(), it will not create a new thread instead, it will execute from the parent thread scope.
🔸 We can override the start() method when extending a class using the thread class, but it won’t give the expected outcome. The reason is that the start() method is responsible to see whether the thread already exists, whether the thread is ready to run, register threads on registers and add them to the thread pool (JVM takes care of these things when the start() method is called).
🔸 But we can override start() and call the super.start() inside to get the normal behavior. But in this case, the class extended using Thread class will start firstly than the main thread.
🔸If you are only overriding the run() method, it is always better to use the Runnable interface. The problem with using the Thread class is if you have some parent-child relationship in our class then, using the Thread class to extend it will break the inheritance hierarchy.
2. Daemon and Non-Daemon Threads
There are 2 types of threads as Daemon and Non-Deamon threads. Daemon threads are low priority threads that always run in the background, and Non-Daemon(User) threads are high priority threads that always run in the foreground. JVM always waits until User threads are done with their execution. But JVM will not wait for Daemon threads to finish their work. It will exits as soon as all User threads are done with their execution. User threads are created by the application and Daemon threads are mostly created by the JVM. The below code segment shows how to set a thread as a daemon and or non-daemon thread.
🔸 The default behavior of the child thread created is always non-daemon in nature. Therefore even the main thread exit child thread will continue to execute.
🔸currentThread().getName() is available only when extends from Thread class. When use implements Runnable, it is not available to us.
3. Thread Constructors
There are several ways to create a thread object from the Thread class by passing different arguments inside the constructor such as Runnable object, Thread Name which is a string, Thread Group object, and Stack Size which is a long. Figure 1 shows different ways of creating a thread object.
4. Thread Scheduling
Execution of multiple threads on a single CPU in some order is called Thread Scheduling. The JRE uses fixed-priority scheduling which, schedules threads based on their priority. Thread priorities are in the range Thread.MIN_PRIORITY which, is 1, and Thread.MAX_PRIORITY which, is 10. Apart from that Thread.NORM_PRIORITY will give 5 as a priority. The chosen thread runs until one of the following conditions is true.
- A higher-priority thread becomes runnable.
- The thread yields, or its run method exits.
- On systems that support time-slicing, the thread’s time allotment has expired.
🔸 The main thread’s default priority is 5 (Thread.NORM_PRIORITY).
🔸 Most people think the default priority of threads created by us is also 5. But the truth is the default priority of the threads created by us is inherited from the parent thread where we create our thread. That is why when we create our custom thread in the main it will also get the thread priority as 5.
🔸 If we set the priority to be more than the MAX_PRIORITY (10) value, we will get an IllegalArgumentException.
5. Thread Lifecycle
Threads can be in one of the five states in their Lifecycle, such as New, Runnable, Running, Blocked, and Dead.
- New: The thread is in the new state when you create an instance of Thread class but before the invocation of the start() method.
- Runnable: The thread is in the runnable state after calling the start() method, but the thread scheduler has not selected it to be the running thread yet.
- Running: The thread is in the running state if the thread scheduler has selected it to run.
- Blocked: The state where the thread is still alive but is currently not eligible to run.
- Dead: The thread is in the dead state when its run() method exits.
6. Thread Join
The join() method allows one thread to wait until another thread finishes its execution. Imagine we have two threads called T1 and T2; if T1 calls the T2.join(). Then the T1 has to wait until the thread T2 completes its actions, and T1 goes to the Block State when called the join() method. The join() method can take milliseconds and nanoseconds as arguments also. If time is passed inside the method when T1 calling the join method as T2.join(2000), the T1 thread will wait until that specified amount of time to see whether T2 completes its action. After that, T1 will continue its actions in the normal way. The T1 will again enter Runnable State from Blocked State only if’
- When T2 completes its job.
- Timeout occurs in T2.join(2000) method.
- When we interrupt the T1 thread.
join(): It will put the current thread on wait until the thread on which it is called is dead.
join(long milliseconds): It will put the current thread on wait until the thread on which it is called is dead or wait for the specified time (milliseconds).
join(long milliseconds, int nanoseconds): It will put the current thread on wait until the thread on which it is called is dead or wait for the specified time (milliseconds + nanoseconds).
7. Thread Sleep
The Thread Sleep method will take the currently executing thread and make it sleep for the specified number of milliseconds. When we call the sleep(1000) method thread will go to the Block State from Running State. When we are calling the sleep(1000) method we have to handle the check Exception call InterruptedException.
- sleep(long milliseconds): sleep the thread for a specified amount of milliseconds.
8. Thread Interrupt
The Thread Interrupt method helps to make the threads in the Blocked State to be Runnable again. When we call the interrupt() method on the Runnable or Running State threads, a flag is set, and those threads will be interrupted as soon as they entered a Blocked State by calling methods like join() or sleep().
9. Thread Yield
By calling the yield() method by a thread, it will allow the other threads a chance to get executed. The JVM thread scheduler is generally fair, but in intensive operations, a thread can monopolize. Adding in a random chance of yielding can improve fairness. Thread Scheduler checks if there is any thread with the same or high priority as the thread that calls the yield() method. If the processor finds any thread with the higher or same priority, then it will move the current thread to Runnable State and give that thread chance to execute, and if not, the current thread will keep executing.
🔸 When we call the yield() method the thread will enter the Runnable State from Running State.
🔸 But when we call the join() and sleep() methods the thread will enter the Block State from Running State.
10. Thread Synchronization
Thread Synchronization is the mechanism that ensures that two or more concurrent threads do not simultaneously execute some particular program segment. Thread Synchronization is mainly needed due to some conditions in program execution called Race Condition and Critical Sections.
- Race Condition: Occurs when two threads access the shared variable at the same time.
- Critical Section: Section of the code that is executed by multiple threads concurrently but this concurrent execution for threads makes different results in the Critical Section.
Java makes it possible that only one thread could execute code segments like above at a given time by adding the synchronized keyword to the method signatures. Therefore only one thread can work with the synchronized methods at a given time, and other threads have to wait till the first thread finishes with their work.
11. Inter-Thread Communication
The process of testing a condition repeatedly until it becomes true is called Polling. We usually use for-loops to implement Polling and check whether a particular condition is true or false. But this wastes much processing time on the CPU and makes the implementation inefficient. To avoid Polling, Java uses three methods called wait(), notify(), and notifyAll(). All these methods belong to the Object Class, therefore all of the classes have access to them. They must be used within a synchronized block of code only.
- wait(): Inform the calling thread to give up the lock and go to sleep until some other thread calls the notify().
- notify(): It wakes up one single thread that is waiting on the same object.
- notifyAll(): It wakes up all of the threads that are waiting on the same object.
For further more clarification check these resources;