– What is thread?
A Thread is a light-weight and smallest unit of processing that can be managed by a scheduler. It is a part of a process that can be executed along with other threads running simultaneously to complete the process efficiently.
– Thread vs Process?
Process is an execution of a program performed by the operating system. Each process can have multiple threads running in parallel to complete parts of the program efficiently.
Thread is the smallest executable unit of a process. It is a subset or a subunit of a process.
Processes run independently without interfering with other processes. Threads also run independently but they can interact/communicate with other threads of the same process. Inter-process communication is complex.
Processes do not share data or memory. Threads share the resources and memory.
Threads are easier to create and manage as they are lightweight.
Processes are managed by PCB (Process Control Block)
Context Switching is easier in Thread whereas process it is difficult to easily switch context.
– How to create a thread in java?
Thread can be created by
- Extending Thread class
We can extend the Thread class to define our own logic of run() and then create a thread in the main class by creating an object of the extended class.
class SimpleThread extends Thread {
@Override
public void run() {
// implement what a thread should do.
}
}
class Main {
public static void main(String[] args) {
SimpleThread thread = new SimpleThread(); // this is a thread.
thread.start();
}
}
- Implementing Runnable Interface
We first implement the run() method which defines what the thread will do, and then in main function we create the thread using Thread class and passing the runnable task.
class SimpleRunnable implements Runnable {
@Override
public void run() {
// implement what a thread should do.
System.out.println(“Inside SimpleRunnable”);
}
}
class Main {
public static void main(String[] args) {
SimpleRunnable runnable = new SimpleRunnable(); // this is runnable
Thread thread = new Thread(runnable); // this is now a thread.
thread.start();
}
}
– What is the run() method in Runnable?
The run() method is where we define the task that the thread will perform. It is the entry point or starting point of a thread. You can think of the run() method as “the main() method of a thread”. Just like JVM invokes the main() method of a class as a starting point of the program, run() method is the same thing for a thread.
- You can invoke run() method multiple times on a thread.
- run() method is defined in the Runnable interface.
- run() method does not create a new thread.
- When you create a thread using runnable like shown above, a thread will be able to call the run method as well.
– What is the start() method in Thread?
start() method is present in a Thread class or any class extending Thread class.
When you create a thread and call start(), it will first create a new thread and then call run() on that same thread. Note that once you have called start() on a thread, you cannot call start() again, it will throw an IllegalThreadState exception. See with an example –
Example 1:
class Main {
public static void main(String[] args) {
SimpleRunnable runnable = new SimpleRunnable(); // this is runnable
Thread thread = new Thread(runnable); // this is now a thread.
thread.start();
}
}
Output:
Inside SimpleRunnable
Example 2:
class Main {
public static void main(String[] args) {
SimpleRunnable runnable = new SimpleRunnable(); // this is runnable
Thread thread = new Thread(runnable); // this is now a thread.
thread.start();
thread.start(); // will throw exception
}
}
Output:
Exception in thread "main" java.lang.IllegalThreadStateException
Example 3:
class Main {
public static void main(String[] args) {
SimpleRunnable runnable = new SimpleRunnable(); // this is runnable
Thread thread = new Thread(runnable); // this is now a thread.
thread.start();
for(int i = 0; i < 2; i++)
thread.run();
}
}
Output:
Inside SimpleRunnable
Inside SimpleRunnable
Inside SimpleRunnable
Explanation:
The output above printed 3 times because, the first time statement is printed because the start() method had inherently invoked run() and then we are additionally calling run() method 2 times in the for loop.
Example 3:
class AlphaRunnable implements Runnable {
@Override
public void run() {
// printing name of the thread
System.out.println(Thread.currentThread().getName() + “ runs AlphaRunnable”);
}
}
class Main {
public static void main(String[] args) {
AlphaRunnable alpha = new AlphaRunnable(); // this is runnable
Thread thread = new Thread(alpha); // this is now a thread.
// without calling start(), it will be a normal function call. No new thread is created.
thread.run();
// now a new thread is created and run() is also called.
thread.start();
}
}
Output:
main runs AlphaRunnable
Thread-0 runs AlphaRunnable
Explanation:
without calling start(), when you can run() directly on a thread it acts like a simple method call on the existing thread without creating a new thread. This is not multi-threading, so no use of calling run() without start(). Also no use of calling run() after start() because run() will already be called implicitly by start().
– What is Runnable?
Runnable is a functional interface with only one abstract method: run() which returns void.
– What is a Functional interface?
A functional interface is one that offers only 1 method that can be implemented by the implementing class. Thus a functional interface can be directly used in lambda functions also. See example below –
Functional Interface:
@FunctionalInterface
interface FuncInterface {
void run();
}
Basic Usage of above Functional Interface:
First we define the implementation of the interface and then create its object in the main class and call the function.
class BasicFuncImpl implements FuncInterface {
@Override
public void run() {
System.out.println(“Implementing FuncInterface”);
}
}
class Main {
public static void main(String[] args) {
FuncInterface basic = new BasicFuncImpl();
basic.run();
}
}
Lambda Usage of above Functional Interface:
In this case we don’t need to define the implementation itself, we can simply copy the overridden function from the BasicFuncImpl, remove keywords public, void and add an arrow(->) between function and its body (in brackets). Since the function only has a single line (sys-out), we can omit the brackets as well. See below –
class Main {
public static void main(String[] args) {
FuncInterface basic = () -> System.out.println(“Implementing FuncInterface”);
basic.run();
}
}
– What is the difference between start() and run()?
run() | start() |
It is defined in Runnable interface | It is defined in Thread class |
It can be called on a thread multiple times. | It can be called only single time and if called again it will throw IllegalThreadState Exception |
run() method defines what a thread will do. | start() method creates a new thread and calls run() on that new thread. |
run() does not create a new thread and simply acts as a normal functional call by the thread that calls run(). | start() creates a new thread and implicitly calls run() |
No use of calling run() on a thread without start() because it is not multi-threading as no new thread is created. | No use of calling run() after start() method also, as start() already calls run() method implicitly. |
– How to get the name of the current thread?
Thread.currentThread().getName()
Returns the name of the current thread.
– How to get the state of the current thread?
Thread.currentThread().getState()
Returns the current state of the current thread.
– Lifecycle of a thread?
– Lifecycle of a thread
– NEW
– RUNNABLE
– BLOCKED
– TIME_BLOCKED/WAITED
– TERMINATED
– What is Thread.sleep()?
Thread.sleep(milliseconds)
This method ceases the execution of the thread for the specified duration without releasing the lock. In other words, Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds. The thread does not lose ownership of any monitors. When a thread enters a synchronized block/method, it acquires a monitor (intrinsic lock).
sleep() is used to delay the execution and since it does not release the monitor/lock, other threads in waiting state will continue to be blocked.
– What is Thread.join()?
– thread.join()
This method waits till the execution of the thread is completed. It is used to ensure that a code is run after the thread has completed its task. For example –
class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println(“Hello World From Thread”));
thread.start();
System.out.println(“Hello World From Main”);
}
}
Output:
Hello World From Main
Hello World From Thread
Explanation:
It can happen that ‘Hello World From Main’ gets printed first because when thread.start() is called, the code executes the remaining code without waiting for the thread to complete its task.
In order to ensure that ‘Hello World From Main’ is printed only after ‘Hello World From Thread’ we can add thread.join() right after thread.start();
– What is Inter-thread communication?
Multiple threads can organise their execution in a coherent manner to achieve a desired output by communicating or interacting with each other. Each thread can call some method(s) to indicate or signal about its own execution state to other threads. This can be done by using methods: wait(), notify() and notifyAll()
– What is wait()?
wait() is a method that is used in a synchronized block which causes the current thread to release the monitor (lock) it holds and enter a waiting state until it is notified (via notify() or notifyAll()). It is used for inter-thread communication like producer-consumer scenarios.
– What is notify() and notifyAll()?
notify() is a method that is used in a synchronized block which wakes up a single thread which is in waiting state to acquire a monitor (lock). If there are multiple threads waiting , one of the
threads in an arbitrary manner get awakened. notifyAll() awakens all the threads in the waiting state.
– wait() vs sleep(ms)
wait() | sleep(milliseconds) |
Release the lock and enters a waiting state until awakened by some other thread using notify() or notifyAll() | Holds the lock (monitor) and pauses the execution for a given duration of time. Reassumes the execution after the time has passed. |
Used for inter-thread communication | Used for delaying execution of a code |
Release the lock allowing other threads to execute their task | Holds the lock and blocks all the other threads until the sleep duration ends. |
– User thread vs Daemon thread?
User threads as the name suggests are created by the user. Each thread we create by ourselves in the program is a user thread (unless we set it as a daemon thread). A user thread is created to perform a primary task and responsible for performing essential tasks. JVM cannot terminate the program without the completion of the user thread. It will keep waiting until the user thread finishes execution.
Daemon threads are threads that run in the background, providing support to user threads. Unlike user threads, daemon threads don’t prevent the JVM from terminating if there are no remaining non-daemon threads. They exist to provide auxiliary services to the user threads, and as soon as all user threads complete, the daemon threads are abruptly terminated, without completing their tasks. Daemon threads are particularly useful for tasks that need continuous maintenance but aren’t critical for the program’s execution. A classic example is the garbage collector in Java, which runs as a daemon thread to reclaim memory occupied by objects that are no longer in use.
We can set a user thread before starting it like – thread.setDaemon()
– What is the critical section?
A section of a code which multiple threads can access simultaneously is called a critical section. In practical cases, threads share resources among themselves and modify these resources. Such sections in shared resources are critical sections as the execution can result in unexpected scenarios. Threads can access critical sections in an unsynchronized manner resulting in ambiguity.
– What is mutual exclusion?
Mutual exclusion (mutex) is the concept that ensures synchronous access to critical sections. Any two threads are mutually exclusive when accessing a critical section. If one thread is inside the critical section, other threads will have to wait to access it.
– What is a lock?
Lock is an object in java that can be used to prevent and allow threads to access a critical section. The technique to use locks to ensure mutual exclusivity is called locking. Each object in java has an intrinsic lock which is used when multithreading is implemented.
– What is the synchronization keyword in java?
The keyword `synchronization` is used to make a critical section code mutually exclusive. In other words, defining a code inside ‘synchronization’ block or adding this keyword to a method, will ensure that any two threads accessing that code block are mutually exclusive. As mentioned every java object has an intrinsic lock, synchronization keyword uses that lock to ensure mutual exclusivity. For example:
class UseSync {
int counter = 0;
public synchronized void increment() {
counter++;
}
}
If two threads arrive at the increment method at the same time, synchronized keyword will ensure only one of the threads is permitted to go inside the increment method. Thus, the result would be counter = 2. Without this keyword, there could be the case when both thread enter at the same time and read counter = 0, and increment it only once resulting in counter = 1.
We can define our own lock implementation exclusively.
– What is a race condition?
When two threads want to access a critical section at the same time, this situation is called ‘race condition’. It means two threads race to perform a certain task.
– What is starvation?
When a certain thread has to wait too long (endlessly) to access a critical section due to a deadlock, it results in starvation.
– What is Reentrant Lock in java?
We can define our own locking implementation using various kinds of locks that java provides. One such lock is called Reentrant Lock. See an example of Reentrant lock –
import lombok.Getter;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
public static void main(String[] args) throws InterruptedException {
CounterClass counterClass = new CounterClass();
CounterThread thread1 = new CounterThread(counterClass);
CounterThread thread2 = new CounterThread(counterClass);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final counter value = "+counterClass.getCounter());
}
}
class CounterThread extends Thread {
private final CounterClass counterClass;
CounterThread(CounterClass counterClass) {
this.counterClass = counterClass;
}
@Override
public void run() {
for(int i = 0; i < 100; i++)
counterClass.increment();
}
}
class CounterClass {
private final ReentrantLock lock = new ReentrantLock();
@Getter private int counter = 0;
public void increment() {
lock.lock(); // acquires the lock if not taken by other thread otherwise thread becomes
// dormant until lock is acquired.
try {
counter++;
} finally {
lock.unlock();
}
}
}
Output:
Final counter value = 200
Explanation:
Thread class CounterThread performs the task of incrementing counter value 100 times. When two threads (1 and 2) reach the method increment, one of them acquires a lock, while the other keeps waiting until the first thread finishes the task.
– What is a Thread Pool?
As the name suggests, thread pool is a set of pre-initialized threads (worker threads) that are ready to be executed and perform a certain task and can be reused. A thread is invoked from a thread pool to perform the task; once that task is completed, the thread is again sent back in the pool. This ensures reusability. In java, we don’t have to define and implement thread pool explicitly and we can use Executor Framework.
– What is Executor Framework?
Thread management can be tedious when the number of threads increases. Handling the creation, maintaining the states and deletion of threads can become a headache. Executor Framework in java is a framework that provides the functionality to create a thread pool and simply define the task to be performed by the thread, without worrying about creation, deletion, reutilisation of the threads. The Executor Framework in the background will handle how and which thread is performing the task whereas the user just needs to submit the task that the thread should perform. See example –
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorFrameworkExample {
public static void main(String[] args) {
// create a thread pool of 3 threads
ExecutorService executorService = Executors.newFixedThreadPool(3);
// define the runnable task that a thread should perform
Runnable runnable = () -> System.out.println("Hello from Thread "+Thread.currentThread().getName());
// submit the runnable to perform the task.
for(int i = 0; i < 6; i++) {
executorService.submit(runnable);
}
executorService.shutdown();
}
}
Output:
Hello from Thread pool-1-thread-1
Hello from Thread pool-1-thread-2
Hello from Thread pool-1-thread-2
Hello from Thread pool-1-thread-2
Hello from Thread pool-1-thread-3
Hello from Thread pool-1-thread-1
Explanation:
We have 3 threads in the pool and a runnable task. This task is performed 6 times, so each time a thread from the pool is picked and the task is performed.
– What is Callable Interface?
Similar to Runnable, Callable is also a functional interface that takes generic type and returns the generic return type.
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
– What is the difference between Runnable vs Callable?
Runnable Interface | Callable Interface |
Functional Interface which is not defined with Generic Type | Functional Interface which is defined with Generic Type like : Callable<V> |
Defines single abstract method run() | Defines single abstract method call() |
run() does not return a value. It has void return type | call() returns the Future<V> data |
Thread takes a Runnable task | submit() method in ExecutorService takes both Runnable and Callable tasks both. |
run() method does not return exception | call() method returns exception |
– ExecutorService : execute() vs submit()?
execute() | submit() |
execute() method return type is void. | submit() return type is Future<V>. In case of runnable, future.get() is null |
Takes only Runnable task | Takes Runnable and Callable tasks both |
run() does not return a value. It has void return type | call() returns the Future<V> data |
Only one method – executorService.execute(runnable) – no return type | Multiple methods like – executorService.submit(runnable) – returns Future<V> with future.get() as null executorService.submit(callable) – returns Future<V> with future.get as output of callable.call() method executorService.submit(runnable, returnVal) – returns returnVal as output after successful completion of runnable task |
Abstract method present in Executor class | Abstract methods present in ExecutorService class which extends Executor class itself. |
– ExecutorService : shutdown()?
ExecutorService when it starts the execution on submit() or execute(), it keeps running to see if there are other tasks to be submitted or executed. We will have to indicate to the executor service that we don’t have any more tasks for it, so we need a shutdown() method to indicate the executorService to stop accepting anymore tasks and only finish the execution of the tasks previously submitted.
shutdown() initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. Invocation has no additional effect if already shut down.
This method does not wait for previously submitted tasks to complete execution.
Note that shutdown() does not wait for all the tasks to be completed, for that we would need to use awaitTermination().
– ExecutorService : awaitTermination(time, unit)?
awaitTermination() takes a time and unit (say 1 in sec) to wait for all the tasks to be terminated within that time frame, if not then the code flow will continue. The method returns true if all the tasks are terminated (or interrupted whichever happens first) before the time frame, false otherwise. If you want to await endlessly, we would have to do –
while(!executorService.awaitTermination(1, Unit.SECONDS)) { // endless waiting }