Converted into a clean HTML study-notes format. Original PDF Pages are intentionally not included.
Multitasking and Multithreading
Multitasking means executing several tasks simultaneously. There are two types:
- Process-based multitasking — several independent programs/processes execute simultaneously.
- Thread-based multitasking — several independent parts of the same program/process execute simultaneously; each independent part is a thread.
Examples
Typing a Java program, listening to audio and downloading files at the same time are examples of process-based multitasking.
Thread-based multitasking is best suited to the programmatic level.
Objectives
- Reduce response/computation time.
- Improve application performance.
Applications
- Multimedia and graphics
- Animations
- Video games
- Web servers and application servers
Advantages of multithreading
- Reduces computation time.
- Improves application performance.
- Threads share the same address space, saving memory.
- Context switching between threads is usually less expensive than between processes.
- Communication between threads has relatively low cost.
What is a Thread?
A thread is a single sequential flow of control within a program. It is also called an execution context or lightweight process.
- A thread is not an independent program.
- A thread runs within a program and uses the resources allocated to that program.
- Each thread has its own execution stack and program counter.
- Java provides built-in multithreading support through
Thread,Runnable,ThreadGroup, and related APIs.
Two ways to create a thread
- Extend
java.lang.Thread. - Implement
java.lang.Runnable.
Creating a Thread by Extending Thread
Steps:
- Extend the
java.lang.Threadclass. - Override
run()to define the work performed by the thread. - Create an instance of the subclass.
- Call
start()to make the thread eligible for execution.
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Child Thread");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("Main Thread");
}
}
}
Flow
Main Thread and Thread Scheduler
- The main thread executes the
main()method. - The JVM creates the main thread.
- If independent jobs exist, multiple threads can be used.
- The Thread Scheduler is part of the JVM and decides which runnable thread gets processor time.
- The exact scheduling algorithm is JVM/platform dependent.
- Therefore, multithreaded programs generally cannot guarantee one exact execution order or output.
start() vs run()
| start() | run() |
|---|---|
| Creates a new thread and makes it eligible for scheduling. | Does not create a new thread. |
The new thread executes run(). | run() executes like an ordinary method call in the current thread. |
| Required to actually start multithreading. | Calling it directly does not provide concurrent execution. |
Why start() is important
start() registers the thread with the scheduler, performs required thread setup, and then invokes run(). It is therefore the key method for starting a Java thread.
start() {
register with Thread Scheduler;
perform required activities;
invoke run();
}
Overloading run()
- Overloading
run()is possible. start()invokes only the no-argumentrun().- Other overloaded versions must be called explicitly.
- If
run()is not overridden, Thread's empty implementation is used.
Overriding start()
Overriding start() causes the overridden method to execute as a normal method call, so a new thread will not be created. It is therefore not recommended.
Thread Lifecycle
↘ Waiting / Blocked ↗
After a thread has been started, attempting to start the same thread again results in IllegalThreadStateException.
Creating a Thread with Runnable
The Runnable approach is performed in three steps:
- Create a class implementing
Runnableand overriderun(). - Create a
Threadobject by passing the Runnable object to its constructor. - Call
start()on the Thread object.
class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("Runnable thread");
}
}
public class Test {
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
t.start();
}
}
Important cases
t.start()creates a new thread and executes the appropriaterun().t.run()does not create a new thread.- A Runnable object itself does not have
start()capability.
Recommended approach
Implementing Runnable is generally recommended because the class remains free to extend another class, avoiding the single-inheritance limitation of extending Thread.
Thread Constructors
| Constructor |
|---|
Thread() |
Thread(String) |
Thread(Runnable) |
Thread(Runnable, String) |
Thread(ThreadGroup, String) |
Thread(ThreadGroup, Runnable) |
Thread(ThreadGroup, Runnable, String) |
Thread(ThreadGroup, Runnable, String, long) |
Hybrid Thread Approach and Thread Name
A hybrid approach can combine inheritance and Runnable implementation. Every Java thread also has a name.
- A thread may have a JVM-generated default name.
- A programmer can provide a custom name.
Thread.currentThread()returns the currently executing thread.
Thread t = Thread.currentThread();
System.out.println(t.getName());
t.setName("MyThread");
System.out.println(t.getName());
Thread Priorities
Every thread has a priority. Java thread priorities range from 1 to 10.
| Constant | Value |
|---|---|
Thread.MIN_PRIORITY | 1 |
Thread.NORM_PRIORITY | 5 |
Thread.MAX_PRIORITY | 10 |
- The scheduler may consider thread priority when allocating processor time.
- A higher-priority thread may get preference, but exact execution order is not guaranteed.
- For equal priorities, scheduling order is also not guaranteed.
- Values outside 1–10 cause
IllegalArgumentException.
Default priority
The main thread has default priority 5. A newly created thread normally inherits its parent's priority.
Yield
Thread.yield() causes the currently executing thread to give other runnable threads of the same priority an opportunity to execute.
- If no suitable waiting thread exists, the current thread may continue.
- The scheduler decides which thread runs next.
- The yielded thread's next execution time cannot be predicted exactly.
- Some platforms may provide limited support for the intended scheduling effect.
join()
If one thread needs to wait until another thread completes, use join().
t2.join();
If thread t1 executes t2.join(), t1 enters a waiting state until t2 completes.
Real-world example
t2 can call t1.join(), and t3 can call t2.join().
Signatures
t2.join();
t2.join(10000);
t2.join(1000, 100);
join() throws InterruptedException, a checked exception, so it must be handled with try/catch or throws.
Deadlock examples
- If main waits for child and child waits for main, both can wait forever.
Thread.currentThread().join()makes the current thread wait for itself indefinitely.
sleep()
If a thread should pause for a specified period, use sleep().
Thread.sleep(10000);
sleep() throws InterruptedException, so it must be handled.
interrupt()
A thread can interrupt a sleeping or waiting thread using interrupt().
child.start();
child.interrupt();
- If the target is sleeping or waiting, the interrupt can immediately affect it.
- If the target is running and never enters a sleeping/waiting state, the interrupt may have no immediate visible effect.
- An interrupt does not forcibly kill a thread; it is a cooperative interruption mechanism.
Yield vs Join vs Sleep
| Property | yield() | join() | sleep() |
|---|---|---|---|
| Purpose | Give other runnable threads an opportunity. | Wait for another thread to complete. | Pause execution for a specified period. |
| Overloaded | No | Yes | Yes |
| Final | No | Yes | No |
| Throws InterruptedException | No | Yes | Yes |
| Static | Yes | No | Yes |
Inter-Thread Communication
Threads can communicate using wait(), notify(), and notifyAll().
- The thread waiting for an update calls
wait()and enters a waiting state. - The thread performing the update calls
notify()ornotifyAll()after updating the shared state. - These methods belong to
Object, because a thread can wait/notify on any Java object whose monitor it owns. - The thread must own the object's monitor, normally by being inside a synchronized section.
- Calling these methods without owning the monitor can cause
IllegalMonitorStateException. wait()releases the object's lock while waiting.
| Method | Releases lock? |
|---|---|
| yield() | No |
| join() | No |
| sleep() | No |
| wait() | Yes |
| notify() | Lock is not immediately released by notify itself. |
| notifyAll() | Lock is not immediately released by notifyAll itself. |
Lock and Monitor
A lock (monitor) synchronizes access to a shared resource.
- A shared resource can be associated with a Java object's lock.
- At most one thread can own that object monitor at a time.
- The lock provides mutual exclusion.
- Every Java object has an associated monitor/lock concept that can be used for synchronization.
Producer–Consumer Problem
The producer thread produces items and puts them into a queue. The consumer thread removes items from the queue.
- If the queue is empty, the consumer waits.
- After producing an item, the producer notifies the waiting consumer.
notify()wakes one waiting thread.notifyAll()notifies all waiting threads, although they still acquire the lock one at a time.- When multiple threads are waiting, the exact thread selected by
notify()is not guaranteed.
Synchronization
Synchronization is a modifier applicable to methods and blocks, not to classes or variables.
If multiple threads operate on the same Java object simultaneously, data inconsistency can occur. The synchronized keyword provides mutual exclusion.
Advantages
- Protects shared data.
- Prevents data inconsistency caused by concurrent access.
Disadvantages
- Increases thread waiting.
- Can reduce performance when used unnecessarily.
Examples
- Public telephone
- Joint bank account
- Online reservation
- Multiple users accessing one shared resource
Internally, synchronization uses an object's lock. A thread must acquire the object's lock before executing a synchronized method/block and releases it after completion.
Object Lock and Synchronized Methods
Locking is based on the object, not simply on the method name.
- Only one thread at a time can execute synchronized code guarded by the same object lock.
- Other threads may still execute non-synchronized methods on the same object.
- If multiple threads operate on the same shared object, synchronization may be required.
- If they operate on separate independent objects, synchronization may not be necessary for that resource.
Class-Level Locking
Every Java class has a class-level lock. A thread executing a static synchronized method requires the class-level lock.
- Only one thread can execute static synchronized methods of that class at a time.
- Once the method completes, the class lock is released.
- Other normal static methods, instance synchronized methods guarded by an object lock, and normal instance methods are governed by their respective rules and locks.
Two levels of locks
- Object-level lock
- Class-level lock
Synchronized Block
If only a small portion of a method needs synchronization, a synchronized block can be used instead of synchronizing the entire method. This narrows the critical section and can reduce unnecessary waiting.
synchronized (this) {
// critical section
}
synchronized (obj) {
// critical section using obj's lock
}
synchronized (MyClass.class) {
// class-level lock
}
Synchronization locks can be based on reference types/objects and class objects, not primitive values.
Race Condition
A race condition occurs when multiple threads access and modify shared state concurrently in a way that can produce inconsistent results.
Synchronization or other concurrency-control mechanisms can be used to protect the critical section.
Synchronized Statements and Interview Questions
The statements inside a synchronized method or synchronized block are called synchronized statements.
Important interview questions
- What is the synchronized keyword and where can it be applied?
- What are the advantages of synchronization?
- What are the disadvantages of synchronization?
- What is a race condition and how can it be resolved?
- What is class-level locking?
- What is object-level locking?
- What is the difference between class-level and object-level locks?
- Can multiple threads execute synchronized methods simultaneously on the same object?
- What is a synchronized block?
- How do you synchronize using the current object?
- How do you obtain a class-level lock in a synchronized block?
- How do you synchronize using a particular object?
- What is the advantage of a synchronized block over a synchronized method?
- Can a thread acquire multiple locks simultaneously? Yes.
- What are synchronized statements?
Deadlock
Deadlock occurs when two or more threads wait indefinitely for locks held by one another.
Thread 2 holds Resource B → waits for Resource A
Result: Both threads wait indefinitely
A classic example is one thread holding a printer-related resource while waiting for another resource, while a second thread holds that other resource and waits for the printer.
Deadlocks are difficult to resolve after they occur, so prevention and careful lock ordering are important.
Starvation
Starvation is prolonged waiting where a thread eventually gets a chance to execute, unlike deadlock where the waiting can continue indefinitely.
For example, a low-priority thread may repeatedly wait while higher-priority work receives processor time.
Daemon Thread
A daemon thread runs in the background to support non-daemon/user threads.
Examples include JVM background activities such as garbage collection and other service threads.
- The main purpose is to provide background support.
- Daemon status can be checked using
isDaemon(). - It can be changed using
setDaemon(true/false). - Daemon status must be changed before the thread is started.
- Changing daemon status after
start()causesIllegalThreadStateException. - The main thread is non-daemon.
- A newly created thread normally inherits daemon status from its parent.
- When the last non-daemon thread terminates, the JVM can terminate remaining daemon threads.
Thread t = new Thread(() -> {
System.out.println("Background work");
});
t.setDaemon(true);
t.start();