Multithreading is the process of executing multiple threads concurrently within a single process.
A thread is a lightweight unit of execution.
Java Program / Process
|
┌────┼────┐
↓ ↓ ↓
Thread1 Thread2 Thread3
Example: A Java application can simultaneously perform background work, handle user interaction, and process data using different threads.
| Process | Thread |
|---|---|
| Independent program in execution | Unit of execution inside a process |
| Has its own memory space | Threads share process resources |
| Relatively heavyweight | Lightweight |
| Communication is comparatively expensive | Communication is easier through shared memory |
| Can contain multiple threads | Exists within a process |
Multithreading is useful for:
- Better responsiveness
- Concurrent execution of tasks
- Better utilization of CPU resources
- Performing background operations
- Handling multiple independent activities
Single Thread:
Task A → Task B → Task C
Multiple Threads:
Thread 1 → Task A
Thread 2 → Task B
Thread 3 → Task C
Important: Concurrent execution does not always mean that every thread literally executes at the exact same instant. Actual parallel execution depends on available CPU cores and scheduling.
A Java thread can move through states represented by Thread.State:
NEW
↓
RUNNABLE
↓
RUNNING / executing
↓
TERMINATED
A thread can also temporarily enter:
BLOCKED
WAITING
TIMED_WAITING
Conceptually:
NEW
|
start()
↓
RUNNABLE
/ | \
↓ ↓ ↓
BLOCKED WAITING TIMED_WAITING
\ | /
\ | /
RUNNABLE
|
execution ends
↓
TERMINATED
Important: RUNNING is commonly used when explaining execution, but Thread.State itself reports RUNNABLE for both ready-to-run and actually running states.
The two classic approaches are:
class MyThread extends Thread
{
public void run()
{
System.out.println("Thread is running");
}
}
class Demo
{
public static void main(String[] args)
{
MyThread t = new MyThread();
t.start();
}
}Output:
Thread is running
class MyTask implements Runnable
{
public void run()
{
System.out.println("Thread is running");
}
}
class Demo
{
public static void main(String[] args)
{
MyTask task = new MyTask();
Thread t = new Thread(task);
t.start();
}
}Output:
Thread is running
Runnable is often preferred because your class can still inherit from another class.
This is one of the most important multithreading doubts.
t.start();Requests that a new thread be started. The JVM then schedules its run() method.
t.run();is an ordinary method call when invoked directly; it does not by itself create a new thread.
class Demo extends Thread
{
public void run()
{
System.out.println("Running");
}
public static void main(String[] args)
{
Demo t = new Demo();
t.run(); // normal method call
t.start(); // starts a new thread
}
}class Demo extends Thread
{
public void run()
{
System.out.println(
Thread.currentThread().getName()
);
}
public static void main(String[] args)
{
Demo t = new Demo();
t.setName("Worker");
t.start();
}
}Output:
Worker
Useful methods:
getName()
setName()
currentThread()
sleep() pauses the currently executing thread for a specified time.
class Demo extends Thread
{
public void run()
{
for(int i = 1; i <= 3; i++)
{
System.out.println(i);
try
{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
public static void main(String[] args)
{
new Demo().start();
}
}Output appears approximately one second apart:
1
2
3
sleep() does not release an intrinsic monitor lock that the thread already holds.
join() allows one thread to wait for another thread to terminate.
class Demo extends Thread
{
public void run()
{
for(int i = 1; i <= 3; i++)
{
System.out.println(i);
}
}
public static void main(String[] args)
throws InterruptedException
{
Demo t = new Demo();
t.start();
t.join();
System.out.println("Main completed");
}
}Conceptually:
Main
|
| start()
↓
Thread
|
| completes
↓
Main continues
Checks whether a thread has been started and has not yet terminated.
System.out.println(t.isAlive());Java provides priorities from:
Thread.MIN_PRIORITY = 1
Thread.NORM_PRIORITY = 5
Thread.MAX_PRIORITY = 10
Example:
t.setPriority(Thread.MAX_PRIORITY);Important: Priority is only a scheduling hint; it does not guarantee which thread executes first.
A daemon thread is a background thread.
Thread t = new Thread(task);
t.setDaemon(true);
t.start();The JVM does not keep running merely because daemon threads remain when all started non-daemon threads have terminated.
setDaemon(true) must be called before the thread is started.
A thread can be requested to stop what it is waiting/sleeping for by interruption.
t.interrupt();The interrupted thread can respond appropriately.
For example, if it is sleeping, InterruptedException may be thrown.
Important: interrupt() is a request/interruption mechanism; it does not forcibly kill the thread.
class MyThread extends Thread
{
public void run()
{
for(int i = 1; i <= 5; i++)
{
System.out.println(
getName() + " : " + i
);
}
}
}
class Demo
{
public static void main(String[] args)
{
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("Thread-1");
t2.setName("Thread-2");
t1.start();
t2.start();
}
}The exact output order is not guaranteed.
Possible output:
Thread-1 : 1
Thread-2 : 1
Thread-1 : 2
Thread-2 : 2
...
Another execution may produce a different order.
When multiple threads access a shared mutable resource, their operations can interfere with each other.
Synchronization is a mechanism used to control concurrent access to shared resources and help maintain consistency.
Shared Resource
/ \
Thread 1 Thread 2
\ /
\ /
Synchronization
|
Controlled access
Example:
class Counter
{
int count = 0;
void increment()
{
count++;
}
}If multiple threads execute increment() concurrently, count++ is not one indivisible operation.
Conceptually:
Read count
↓
Add 1
↓
Write count
Two threads can interfere with these steps, producing an unexpected result.
This is a race condition.
class Counter
{
private int count = 0;
synchronized void increment()
{
count++;
}
int getCount()
{
return count;
}
} synchronized provides mutual exclusion around the method's execution for the relevant object monitor.
Instead of synchronizing the entire method:
synchronized(this)
{
count++;
}Example:
class Counter
{
int count = 0;
void increment()
{
synchronized(this)
{
count++;
}
}
}This can reduce the synchronized portion to only the critical section.
For a static synchronized method:
static synchronized void test()
{
}the lock is associated with the Class object, rather than an individual instance.
Conceptually:
synchronized instance method
↓
object monitor
static synchronized method
↓
Class object's monitor
For:
synchronized void test()
{
}the thread acquires the monitor associated with the object on which the method is invoked.
Object
|
└── Monitor lock
|
Thread enters
↓
executes
↓
releases lock
Only one thread at a time can hold that particular monitor.
These methods are associated with an object's monitor.
A thread waits and releases the monitor.
Wakes one waiting thread.
Wakes all threads waiting on that monitor.
Example:
class Demo
{
synchronized void test()
throws InterruptedException
{
System.out.println("Waiting");
wait();
System.out.println("Resumed");
}
synchronized void wake()
{
notify();
}
}wait(), notify(), and notifyAll() must be invoked while the current thread owns the corresponding object's monitor, otherwise IllegalMonitorStateException occurs.
sleep() |
wait() |
|---|---|
Method of Thread |
Method of Object |
| Used for timed suspension | Used for inter-thread coordination |
| Does not release an intrinsic monitor lock | Releases the object's monitor |
| Can be called without owning a monitor | Must be called while owning the corresponding monitor |
| Usually resumes after time expires, unless interrupted | Waits until notified/interrupted or timeout occurs |
A deadlock occurs when threads become permanently blocked because each is waiting for a resource held by another.
Thread 1
|
holds Lock A
↓
waiting for Lock B
↑
|
Thread 2
|
holds Lock B
↓
waiting for Lock A
Neither can proceed.
Threads can coordinate using:
wait()
notify()
notifyAll()
Typical pattern:
Producer
|
↓
shared resource
↑
|
Consumer
The producer can notify consumers when data becomes available, while consumers can wait when the resource is unavailable.
volatile is used when a variable is shared between threads and visibility of updates matters.
class Demo
{
volatile boolean running = true;
}A write to a volatile variable becomes visible to other threads according to Java's memory model.
volatile does not make compound operations such as count++ atomic.
volatile int count;
count++; // still not atomicFor such operations, synchronization or an appropriate atomic class may be required.
Java provides classes such as:
AtomicInteger
AtomicLong
AtomicBoolean
Example:
import java.util.concurrent.atomic.AtomicInteger;
class Counter
{
AtomicInteger count =
new AtomicInteger(0);
void increment()
{
count.incrementAndGet();
}
}These provide useful atomic operations without manually synchronizing every operation.
A class is considered thread-safe when its behavior remains correct when accessed concurrently according to its contract.
Ways to achieve thread safety include:
Synchronization
Immutable objects
Atomic classes
Concurrent collections
Proper confinement
Multithreading is broader than manually creating Thread objects.
Java also provides:
ExecutorService
Future
Callable
CompletableFuture
ConcurrentHashMap
BlockingQueue
Atomic classes
Locks
Semaphores
CountDownLatch
CyclicBarrier
For larger applications, executors and concurrency utilities are generally preferred over manually creating many threads.
Thread |
Runnable |
Callable |
|---|---|---|
| Represents a thread | Represents a task | Represents a task |
run() |
run() |
call() |
No return value from run() |
No return value | Can return a value |
Thread itself is a class |
Functional interface | Functional interface |
| Direct thread management | Separates task from thread | Supports result/exception through Future |
MULTITHREADING
|
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
Thread Creation Life Cycle
| | |
currentThread() Thread class NEW
getName() Runnable RUNNABLE
setName() Callable BLOCKED
sleep() WAITING
join() TIMED_WAITING
interrupt() TERMINATED
priority
daemon
|
↓
Synchronization
|
┌────┼───────────────┐
↓ ↓ ↓
synchronized Locks Atomic
| | |
method/block Lock API AtomicInteger
|
↓
Shared Resource
|
↓
Race Condition
|
↓
Thread Safety
|
├── wait()
├── notify()
└── notifyAll()
MULTITHREADING
|
├── Thread = lightweight unit of execution
|
├── Creation
| ├── Thread
| └── Runnable
|
├── Important methods
| ├── start()
| ├── run()
| ├── sleep()
| ├── join()
| ├── interrupt()
| ├── getName()
| └── setName()
|
├── States
| ├── NEW
| ├── RUNNABLE
| ├── BLOCKED
| ├── WAITING
| ├── TIMED_WAITING
| └── TERMINATED
|
├── Problems
| ├── Race condition
| └── Deadlock
|
├── Synchronization
| ├── synchronized method
| ├── synchronized block
| └── static synchronization
|
├── Communication
| ├── wait()
| ├── notify()
| └── notifyAll()
|
├── Visibility
| └── volatile
|
├── Atomicity
| └── Atomic classes
|
└── Modern concurrency
├── ExecutorService
├── Future
├── Callable
├── CompletableFuture
├── Concurrent collections
└── Locks
start() → starts a new thread
run() → ordinary method call when called directly
sleep() → pauses current thread; does NOT release monitor
wait() → waits AND releases corresponding monitor
notify() → wakes one waiting thread
notifyAll() → wakes all waiting threads
synchronized → mutual exclusion around protected code
volatile → visibility, NOT general atomicity
Race condition → unsafe concurrent access
Deadlock → threads permanently wait for each other
Thread → execution mechanism
Runnable/Callable → task abstraction
join() → wait for another thread to terminate
interrupt() → request interruption; does not forcibly kill