This tutorial covers the overview of multithreading in Java (Concurrency in Java) and the differences between multithreading and multiprocessing. Before we go into detail about multithreading, let us understand what is a thread.
Table of Contents
Java Thread
A thread is a lightweight process or the smallest unit of a process. Every process can have multiple threads that execute different tasks. There are different ways of creating a thread that we will understand in detail in the next tutorial. A thread is of two types: a user thread or a daemon thread. Whenever we create a new application, it creates a new single thread after which we can create multiple threads of any type.
Now, that we know what is a thread, let’s see in detail about multithreading Java and concurrency.
Multithreading in Java (Concurrency in Java)
When we execute multiple threads at the same time, we call it Multithreading. In other words, we can say it is a Java technology that allows multiple parts of the program to perform simultaneously. This results in maximum CPU utilization. We can also call it concurrency since it executes two or more threads concurrently. In the multithreading process, the threads run in parallel and it is easy to switch between the threads. Multithreading saves memory as it does not allocate separate memory for each thread.
We can implement multithreading concepts using the classes present in the Java concurrency package java.util.concurrent.
Advantages of Multithreading in Java (Advantages of Concurrency)
- Each thread is independent and hence if an exception occurs in one thread, it does not affect the other thread.
- Executes multiple tasks at the same time and hence saves time
- Since threads are independent, it does not block the user and can perform many operations simultaneously
- Saves memory since multiple threads do not allocate separate memory
- Maximum CPU utilization
- Better responsiveness and provides a better user experience
Multitasking
Multitasking means executing multiple tasks simultaneously. This means, it shares the single CPU across multiple tasks and switched between the tasks whenever required. There are 2 types of multitasking:
- Process-based multitasking (Multiprocessing)
- Thread-based multitasking (Multithreading)
Process-based multitasking
In multiprocessing, every process has a separate memory allocation. Each process is heavy-weight and hence switching between processes is very time-consuming. Hence communication between them is also very high.
Thread-based multitasking
In multithreading, multiple threads share the same memory. Each thread is a lightweight process and hence communication between threads is low.
The below diagram illustrates the concept of multiprocessing and multithreading.
Creating a thread
We can create a thread in two different ways. Both the methods override the run() method.
- Implementing Runnable interface
- Extending Thread class
Runnable interface
To create a runnable thread, we can implement the Runnable interface and override the public run()
method. To access this method, we need to create a Thread object and invoke the start()
method. We normally use the Runnable interface when we want to implement more functionality in a class.
Thread class
Another way of creating a runnable thread is to extend the Thread class and implement the public run()
method. Using the Thread object, we can invoke the start()
method to execute the run()
method and run the thread. We use the Thread class when we want to implement only the Thread functionality.
Thread States/Lifecycle of a thread
A thread has different states and traverses through each state in different stages as described below:
- New: When we create a new thread, it is in a new state before we call the start() method.
- Runnable: A thread takes this state when it calls the start() method and waits for the scheduler to pick up the thread.
- Running: When the thread is in execution, it is in a running state.
- Waiting: Thread is in waiting state when it has to wait for other threads to complete execution as part of the synchronization process.
- Dead: When the thread is terminated, it is in the dead state.
A simple example of multithreading in Java (Concurrency in Java)
Below is a simple example of multithreading where we create a thread by extending the Thread class. We create 2 different threads and execute them using the start()
method. When it executes the start()
method, it internally automatically invokes the run()
method and executes them. We will see in detail various Thread methods in the next tutorial.
public class ThreadDemo extends Thread { public void run() { System.out.println("Thread " + Thread.currentThread().getId() + " running"); } public static void main(String[] args) { ThreadDemo t = new ThreadDemo(); ThreadDemo t1 = new ThreadDemo(); t.start(); t1.start(); } }
Thread 13 running Thread 12 running
Multiprocessing vs Multithreading in Java
Multithreading | Multiprocessing |
---|---|
Executes multiple threads within a single process concurrently | Executes two or more processes simultaneously |
Creates computing threads | Increases computing power |
Creation of thread is economical | Creation of process is slow |
Shares same memory | Allocates different memory for each process |
Takes moderate time for processing | Takes less time for job processing |
It does not have any classification | It may be classified as either symmetric or asymmetric |
Thread wait, notify, notifyAll()
In Java concurrency, a lock is an important concept to implement synchronization. When 1 thread is using a shared resource, it locks it until it completes the execution so that other threads don’t access it. Hence it is necessary to obtain the lock status of a shared resource. We can obtain this information or implement it using the 3 methods of the Object class which is wait()
, notify()
and notifyAll()
. The wait() method puts the thread into a waiting state whereas notify() and notifyAll() methods wake up the threads from the waiting state to resume the execution.
Java thread-safety and synchronization
Java supports the concept of multithreading and hence there are chances that it may lead to data inconsistency. This is because multiple threads share the same resource and hence thread safety is a very important topic. Thread safety and synchronization need to be handled since the operations performed using threads are not atomic in nature. There are different ways to make Java program thread-safe which we will see in upcoming tutorials.
Thread safety and immutability
To ensure thread safety in a multithreaded environment, we need to make the shared resources as immutable. In this way, we can ensure that multiple threads do not update the value of shared resources when it is made immutable. However multiple threads can read the shared resources.
Java Memory model
Java memory model is an important concept that we use to understand how to design concurrent programs correctly. It depicts how the JVM works with the RAM(Random Access Memory). This memory model takes care of the thread synchronization and allows the threads to know when and how the threads have written values to the shared variables. The Java memory model contains both Thread stack and heap memory where every thread maintains its own thread stack.
Java volatile keyword
In multithreading, we need to ensure that the threads always read or write the values of the shared variables from the main memory instead of the CPU cache. This is because if a computer has more than 1 CPU, each thread can maintain its own shared variable value in the CPU cache which will result in inconsistency. To avoid this problem, we can declare the shared variables as volatile using the volatile
keyword so that we can ensure that the threads always read/writes the values only from the main memory and not from the CPU cache.
Java exception in the main thread
In any Java program, the first thread that the JVM creates is the main method. Sometimes the main method can throw an exception which we need to handle accordingly. We will understand the various exceptions in the main thread and how to handle them in a separate article.
Daemon threads
Daemon thread is a type of thread that executes tasks in the background. It destroys the daemon threads automatically when all the user threads are completed and terminates the program. I will cover in detail Daemon threads in a separate article.
Java Thread Local
Java Thread Local helps to create thread-local variables in multithreading. By using thread-local, it avoids the synchronization process. This article details Java Thread Local.
Thread signaling
Thread signaling allows threads to send signals between the threads for better communication. It can also enhance the thread communication by sending signals informing whether the thread has to wait for some process to complete so that it can utilize it.
Java Thread Dump
We can use the Java Thread dump to retrieve the thread details and information. This helps in a multithreading environment to analyze the performance issues with the applications. It also helps in identifying and fixing deadlock situations. This article details different ways to generate thread dump.
Java Timer class
In multithreading, we can schedule thread execution at definite time intervals using the Java Timer class and TimerTask classes. This article details the usage of the Java Timer class and how to cancel the timer.
Java Thread Pool
A Thread pool is a group of threads waiting to process a job. We can create a thread pool using the Java Executor framework that has the Executors and ThreadPoolExecutor classes. This concept was introduced from Java 5.
Java Callable – Future
Java Callable and Future interfaces is a common concept in multithreading concept. We can use this mainly when we want the threads to return some values. This is part of the concurrency package and is available from Java 5 onwards. We can use the Future object to find the status of the Callable tasks. The Callable tasks return a future object.
Thread scheduler
The thread scheduler is part of the JVM that decides the sequence of thread execution. We cannot predict which thread it picks for execution and sometimes depends on the thread priority as well. Generally, the thread scheduler uses either preemptive or time-slicing to schedule threads.
Thread priority
Every thread has a priority which is an integer value between 1 and 10. In many situations, the thread scheduler picks up the threads for processing the tasks based on the thread priority. There are 3 different constants defined for thread priority: MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY.
Components in Java Concurrent package
Next, we will see what are the main components that are part of the Java concurrent package java.util.concurrent
Executor
Executor interface executes a specific task. Based on the implementation, it decides whether to execute the task on the new thread or the current thread.
ExecutorService
ExecutorService is used for asynchronous processing. Based on the availability of the thread, it schedules the submitted task. We use the Runnable interface to schedule the task.
ScheduleExecutorService
ScheduledExecutorService schedules the tasks periodically without any delay. This means it executes the tasks instantly. We can use the Runnable and Callable interfaces to define the task.
Future
Future provides the result of the asynchronous tasks. We can use this interface to retrieve the computed result, check if the task is complete or not.
CountDownLatch
CountDownLatch is a utility class that is part of the JDK 5 package which makes the threads wait until some other operations are completed. It generally has a counter which is an integer value that decrements automatically as and when each dependent thread completes the execution. When the counter value is 0, it releases all the threads.
CyclicBarrier
CyclicBarrier is similar to CountDownLatch except that it allows multiple threads to wait and supports reusability. We can define the task using the Runnable interface.
Semaphore
Semaphore is used to block the thread access to critical sections. Before the thread enters the critical section, it checks whether the semaphore has a permit or not. If there is a permit, it allows the thread to enter else it will not. It contains a method to check the number of available permits. Semaphores can also be used for thread signaling.
- Counting semaphore: This type of semaphore can count the number of signals sent using the
take()
method. - Bounded semaphore: It is a semaphore that has an upper bound of the number of signals it can store
ThreadFactory
ThreadFactory is a thread pool that helps to create a new thread based on the requirement.
BlockingQueue
BlockingQueue is useful in asynchronous scenarios where there is a producer-consumer problem which is a serious issue in the Java Multithreading environment.
DelayQueue
DelayQueue is a BlockingQueue of infinite size. This introduces a delay in polling the element which means the topmost element in the queue which is the head element will be removed only in the last.
Locks
A lock ensures that only the currently executing thread has access to the shared resource and blocks other threads from accessing it until the current thread releases the resource. This is mainly used for synchronization and we can use its methods like a lock() and unlock() in separate methods.
Phaser
Phaser is also a flexible barrier that blocks multiple threads for execution. It executes a number of threads dynamically in separate phases.
Issues in Multithreading or Concurrency
Multithreading or concurrency can result in several serious issues due to the concurrent execution of multiple threads. Below are few issues that arise due to multithreading.
Java Deadlock situation in a multithreading environment
Deadlock is a serious situation that arises in a multithreading environment in Java. This occurs when 2 threads infinitely waits for their execution to complete. In this case, both the threads are locked infinitely never executes since it waits for each other. This article details the deadlock situation.
Producer-Consumer problem
The producer-consumer problem is a common issue seen in the multithreading environment in Java. We can solve this issue using the wait()
and notify()
methods until Java 5. After Java 5, we can solve this problem using the BlockingQueue. Producer-consumer problem solution waits for the queue to be non-empty before accessing any element and ensures there is enough space before adding any element.
Race condition and critical section
A critical section is a section of code that multiple threads execute. A race condition occurs in a critical section when multiple threads read/write the same variables. It also means that the threads are in a race and when there is a difference in the sequence of the thread execution, the result can have an impact on the concurrent execution of the critical section. There are 2 patterns that can occur in a race condition:
- Read-modify-write
- Check-then-act
Starvation and Fairness
Starvation is another issue that occurs in Java concurrency which means the thread is not allowed CPU time since other threads grab it. Hence the thread is said to be starved. The solution for this problem is fairness which means all threads are fairly given a chance for execution.
Nested monitor lockout
Nested monitor lockout is similar to a deadlock situation where the threads wait forever. The only difference is that the threads are locked in the same order in a nested monitor lockout whereas, in a deadlock situation, the threads are locked in a different order.
Slipped conditions
The slipped condition occurs in a situation where the thread has checked a condition and before it acts upon it, another thread changes the condition which results in an error for the first thread. This is another issue that arises out of multithreading in Java.
Java Concurrency constructs to resolve the concurrency issues
Below are the few constructs that help to resolve the concurrency issues in Java that we have seen above:
Deadlock prevention
We have seen that deadlock occurs when multiple threads try to access the same lock in a different order. However, we can prevent deadlock by following the below methods:
- Lock ordering
- Lock timeout
- Deadlock detection
Locks
A lock is a synchronization mechanism in Java multithreading that locks a particular shared resource from further access by other threads when it is currently used by the executing thread. There are several lock implementations that use the lock()
method to prevent further access and unlock()
method to release the lock.
Read/Write lock
Read/write lock is required when we have multiple threads reading and writing the same resource. When a thread is reading a resource and another writes the same resource, it can result in ambiguity. Hence to solve this issue, we can implement a read/write lock that does not allow any other read or write access when the current thread writes to it.
Reentrant locks
Reentrant means a thread can retake a lock for the already existing lock. An example of reentrant is Java’s synchronized blocks. This can result in a lockout similar to a deadlock situation if not handled properly.
Thread synchronization in Java
Thread synchronization is an important concept in the Java multithreading environment. Synchronization ensures that at a time only one thread can access the shared resource. Below are the various synchronization implementations. It prevents thread interference and consistency problem.
Synchronized method
A synchronized method is a method that is declared with the synchronized
keyword. When the thread calls a synchronized method, it locks the shared resource and releases it only when the thread terminates.
Synchronized block
A synchronized block is a block that is defined with the synchronized
keyword. The scope of the synchronized block is smaller than the method. We can synchronize any shared resource within the synchronized block.
Static synchronization
A static method that we declare with the synchronized keyword is a static synchronization. In this case, it will lock the class instead of the specific object.
Inter-thread communication
The communication between threads to enable synchronization is inter-thread communication. This mechanism occurs when a thread in a critical section pauses during the execution and allows another thread to enter the critical section. For this we can use either the notify(), notifyAll(), and wait() methods.
Interrupting thread
We can interrupt a thread from the sleep or wait state using the interrupt() method which throws InterruptedException since it moves out from the sleeping and waiting state. If we call the interrupt() method when the thread is not sleeping or waiting, it will execute as normal without any exception and sets the interrupt flag as true.
Reentrant monitor
Reusing the same monitor again for synchronization is called a reentrant monitor. This can avoid single thread deadlocking in Java concurrency.
Conclusion
In this tutorial, we have seen what is multithreading in Java or concurrency in Java, multitasking, and also seen an overview of thread-related concepts like Thread creation, thread pool, thread priority, thread dump, thread scheduler, deadlock situations. In the upcoming tutorials, we will see in detail these topics.
You can refer to the Multithreading interview questions to prepare for interviews that cover the top and important questions.