Multithreading In Java
Before we jump into understanding Multithreading, let’s begin with Multitasking.
Multitasking is an everyday layman term. To put it simply, if I’m doing multiple things at the same time — that’s multitasking. Listening to radio while brushing my teeth? Multitasking. Talking to my best friend on the phone while hitting those 10k steps? Multitasking.
You get the picture. Multitasking is a great way to get multiple things done in less amount of time, or to formalize it, doing things efficiently. If I’d be doing all these tasks one at a time, it might have taken 10 hours of my day, however with multitasking, I saved a lot of time. This is exactly what happens in our digital devices as well.
Multitasking doesn’t mean I’m focusing on all the tasks at the same instance, but I’m dividing my time in switching between different tasks.
What is a Process vs Thread?
Many a times, we switch between different applications on our devices — I might be writing a document and then switching over to Chrome (not a paid advertisement.) to look up something.
Here, the program that is currently active and executing is what is called a PROCESS.
Now, within this single process there can be multiple sub processes.
e.g. : Cooking a dish is a process.
This process can have multiple sub-processes like washing and cutting the ingredients, heating the pan, plating the dish etc.
A single sub-process in the currently executing program/process is called a THREAD.
Thread can also be understood as an independent path of execution within a program.
Threads inside a single program have access to the same data and code because they share the same memory space.
What is Multithreading?
When multiple threads (sub-processes) in a program are executing concurrently, it is called MULTITHREADING.
It’s the same as Multitasking, as seen earlier. However, Multitasking was occurring between different programs. When the same multitasking occurs in a single program and between different sub-processes, that’s MULTITHREADING.
Ever written a frustrated message and it got auto-corrected to “For duck’s sake”? Yup, Multithreading — writing and auto-correction occurring at the same time.
Browsing the internet while there’s a download going on in the background. Multithreading.
Listening to your crush’s voice note and texting a real time update to your best friend? Multithreading. You get the gist.
Why Multithreading?
CPU is utilized while running any program.
If your program has two sub-processes to be run, taking the analogy from cooking a dish:
One (AKA Thread 1): Cutting vegetables — carrots, potatoes, peas.
Two (AKA Thread 2): Heating the pan
Now, we are out of carrots and hence sub-process one cannot be completed until we order more carrots.
However, we see that heating the pan is in no way related to whether we have vegetables.
So, we have two ways to proceed with Threads 1 and 2:
A. Either, we wait for the carrots to be delivered — cut the veggies and only after Thread 1 is completed, we move to Thread 2.
In this scenario, there is a wait time/idle time between ordering the carrots and having them delivered and we are just waiting.
OR
We can use the time that we spend waiting, make it efficient :
We cut the available vegetables, put it on hold.
and proceed with Thread 2, and then once we have the carrots available continue with Thread 1 again.
In this way, we have made efficient use of time. Similarly, a lot of time the CPU spends idle time in either waiting for the user’s input or letting a program process and complete. With multithreading, we have efficient CPU Utilization as it keeps switching between multiple threads.
Types of Threads
There are primarily two types of threads:
1. User Threads
Threads that perform primary tasks.
2. Daemon Threads
Threads that run in the background.
The starting point of java applications — main() method is also a user thread.
The difference between user and daemon threads is that JVM won’t stop executing the program until all the user threads have completed execution whereas Daemon threads are at the mercy of JVM.
The threads do not execute in a chronological or orderly way, the execution is completely decided by the JVM.
An application where multithreading hasn’t been implemented is called a SINGLE-THREADED APPLICATION because of main() method which is also considered a thread.
User created threads are spawned off from the main thread. The threads that might be spawned off from other threads hence share a PARENT-CHILD relationship and referred to as PARENT THREAD and CHILD THREAD respectively.
How to Create Threads?
Threads can be created by 2 possible ways:
- Extending the java.lang.Thread Class
2. Implementing the java.lang.Runnable Interface
Since Runnable is a Functional Interface, we can also provide the method definition as a Lambda Function instead of creating a separate class that implements Runnable.
start() method doesn’t automatically start the thread execution. It is an asynchronous method which signals to the JVM that the specific thread should be added in queue to execute.
The JVM decides when the thread will run and internally calls the run() method that we have overridden above.
Which is better — Runnable or Thread?
Implementing Runnable is considered better than extending the Thread class due to the issues related to multiple inheritance in Java.
If the class extends the Thread class, it doesn’t have the freedom to extend any other classes however, Java permits implementing multiple interfaces.
Hence, with this — our code can still achieve multithreading by implementing Runnable instead of extending the Thread class.
Race Condition
A RACE CONDITION occurs when multiple threads try to access the shared resource at the same time. Since multiple threads are “racing” to make updates/modifications to the shared resource; the program output depends on the timing or order in which the threads executed. This can often lead to data inconsistencies and incorrect data.
Example:
There is a bank account in which money can be credited or debited. Multiple threads might be working concurrently to access these methods and modify the data leading to incorrect data or inconsistencies.
To demonstrate the same, below is a BankAccount class which has the balance initialized to 1000. This class acts as the shared resource which can be accessed by multiple threads.
It has 2 methods, credit and debit.
Driver Method:
Our driver code has two different threads called credit and debit.
The thread credit tries to credit 2000 every iteration for 2 runs, i.e. credit 4000. Hence, the balance after credit should reflect 5000.
The thread debit tries to debit 1000 every iteration for 2 runs, i.e. debit by 2000. Hence, the balance after debit should reflect 5000–2000 = 3000.
We also call the join() method which is used to state that the thread that called this thread (in this case, the main thread) has to wait for execution and will remain in waiting state until this referenced thread terminates.
As we see since credit and debit threads can access the shared resource BankAccount concurrently, the final balance displayed is 5000 which is incorrect.
Get Eshika Arya’s stories in your inbox
Join Medium for free to get updates from this writer.
In order to solve the race condition, a concept called Synchronization is introduced.
Synchronization
As we saw in the scenario above, there can exist critical conditions where we want to control access to the shared resource to one thread at a time.
The mechanism used to coordinate multiple threads and their access to the shared resource is called SYNCHRONIZATION.
There are 3 main ways to achieve synchronization:
- Synchronized Methods
- Static Synchronization
- Synchronized Blocks
Synchronized Methods: Lock the entire method to be accessed by only one thread at a time.
This often slows down the process, since the entire method has been locked, even if it might contain block of code that doesn’t need synchronized access.
2. Static Synchronization: Is applied to methods or variables of a class that are static. Uses class-level locks (more on locks later.)
3. Synchronized Blocks: Only the block defined as synchronized allows the thread to access it one at a time. This improves performance and program efficiency since the entire method is not being blocked.
synchronized(this) is using an object-level lock where the lock is the current instance of the class/current object calling the class method.
Locks
Locks in multithreading are used to restrain access to shared resources to only one thread among multiple threads.
Since the resource can be accessed by only one thread at a time; it helps preventing race conditions and data corruptions.
- Object-Level Locks:
This is a concept where in every object of the class has a unique lock. The synchronized(this) block we saw earlier uses a lock of object-level type with this referring to the current instance of the class. Hence for that block of code, if a thread wants to execute it on a specific object of the class; it will have to acquire the lock for the specified object.
2. Class-Level Locks:
As the name suggests, these locks are related to the entire class instead of a specific instance of the class. Therefore, any thread that wants to execute the static synchronized methods of the class will need to acquire this class-level block. While the specific thread having this lock is executing the method, no other threads are allowed to execute any of the static synchronized methods of the class. Once the execution of the method ends, the thread automatically releases its acquired lock.
synchronized(ABC.class) has a lock of class-level.
Volatile and Atomicity
Atomicity in Multithreading ensures that a series of actions can be completed by a thread without interruptions from other threads.
Synchronization and locks are some of the ways to achieve atomicity as it helps restrain access to the block of code to only one thread at a time.
As we noticed earlier with the credit and debit example, without atomicity in place — multiple threads can simultaneously modify or read the data which would not be consistent or correct.
e.g:
If two threads simultaneously work on incrementing the count of a shared resource, without atomicity — both of these threads might read the same value 1 and increment it to the same value 2; leading to a loss of 1 increment value.
…..
Volatility in Multithreading helps in resolving visibility challenges. Volatile keyword in Java ensures that changes made by one thread to the shared resource is immediately visible to other threads.
Since the threads have local caching of variables, without volatility they won’t be able to immediately see the updates made to shared resource by another thread.
However, volatility ensures visibility and the above two threads work in the below manner.
One thread reads and increments the value of the counter, the other thread reading this value will be able to immediately see the updated counter value and increment accordingly.
ACID: Atomicity, Consistency, Isolation, Durability
In Multithreading, ACID acronym helps guarentee concurrency.
- A for Atomicity:
All operations are considered as a TRANSACTION. Just like a bank transaction, where individual operations such as debit and credit amount are considered as one transaction. If debit is successful and credit fails, then the entire transaction is reversed. An “all or nothing” principle.
In multithreading as well, it follows the same. It helps ensure the failures due to individual operations don’t cause partial, incomplete changes to data .
2. C for Consistency:
Ensuring data transitions from one valid state to another strictly obeying required rules and constraints.
e.g. : When trying to save a variable in database that might have a negative bank balance or has a duplicate primary key, JPA will trigger a failure, keeping the transaction in a valid state.
3. I for Isolation:
Isolation ensures that concurrent threads do not interfere with each other.
4. D for Durability:
Ensures that transactions once successfully stored survive permanently — even through power cuts or severe system downtimes.
Deadlock
A situation in Multithreading where two or more threads are permanently blocked because they are waiting for each other to release an acquired lock. This causes the threads to get permanently stuck, and the program never continues execution.
Thread Lifecycle
The various states in which a thread exists from its creation till termination is called lifecycles of a thread.
- NEW
A Thread that has been instantiated but start() method has not been called on it yet.
Thread t1 = new Thread(runnableObject);2. ACTIVE STATES:
a. RUNNABLE
A thread on which start() has been called and is waiting for CPU time slice allocation.
b. RUNNING
A thread that is actively executing.
3. BLOCKED
An active thread enters this state when it is alive and ready to execute but cannot proceed working with a block of code that exists within synchronized block since it is waiting for a lock that is currently being held by another thread.
4. WAITING
The threads enters an indefinite waiting state when it surrenders control to wait for another thread to perform its action and stays here until it is notified with a release signal.
Triggered by: Object.wait(), Thread.join()
Released by: Another thread calling Object.notify(), Object.notifyAll()
5. TIMED WAITING
This is also similar to waiting state however has a threashold limit after which it is released either by the timeout or by receiving a release signal by another thread.
Triggered by: Thread.sleep(timeDuration), Thread.join(timeDuration), Object.wait(timeDuration)
6. TERMINATED
The end of a thread’s lifecycle either naturally once the run() method logic is completely executed or if it is unnaturally aborted due to an unhandled runtime exception.
Once the thread ends, it can’t be restarted by calling the start() method. Doing so leads to triggering IllegalThreadStateException.






