Implementing Multithreading in Java: Core Concepts and Practical Examples
Multithreading Fundamentals
Concurrency vs. Parallelism
- Parallelism: Refers to multiple events occurring simultaneously at the same instant, typically enabled by multiple CPU cores where each core executes a separate task concurrently.
- Concurrency: Involves multiple evants happening within the same time interval, but not necessarily at the exact same moment.
Process and Thread
- Process: An executing application with its own dedicated memory space. Each process represents a program's execution instance and serves as the fundamental unit managed by the operating system. A single application can spawn multiple processes.
- Thread: An independent execution unit within a process. Multiple threads can operate concurrently inside a single process, sharing resources while maintaining separate execution flows.
Key Differences Between Process and Thread
- Process: Has isolated memory (heap and stack), operates independently, and contains at least one thread.
- Thread: Shares heap memory with other threads in the same process but maintains a private stack. Threads are more resource-efficient compared to processes.
Thread Scheduling
With a single CPU, only one instruction executes at any given time. Multithreading involves rapid context switching where threads take turns using the CPU in tiny time slices. The JVM employs preemptive scheduling, leading to unpredictable execution order among threads.
Approaches to Create Threads in Java
Extending the Thread Class
Constructors
Thread(): Creates a new thread object.Thread(String name): Creates a named thread object.
Key Methods
String getName(): Retrieves the thread's name.void start(): Initiates thread execution by invokingrun().void run(): Contains the thread's task logic.static void sleep(long millis): Pauses the current thread for specified milliseconds.static Thread currentThread(): Returns a reference to the currently executing thread.
class WorkerThreadA extends Thread {
@Override
public void run() {
for (int count = 0; count < 40; count++) {
System.out.println(getName() + " processing task " + count);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class WorkerThreadB extends Thread {
@Override
public void run() {
for (int count = 0; count < 40; count++) {
System.out.println(getName() + " handling operation " + count);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
WorkerThreadA threadA = new WorkerThreadA();
WorkerThreadB threadB = new WorkerThreadB();
threadA.setName("Worker-A");
threadB.setName("Worker-B");
threadA.start();
threadB.start();
for (int i = 0; i < 50; i++) {
System.out.println("Main thread executing " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Implementing the Runnable Interface
Constructors
Thread(Runnable target): Creates a thread with a specified Runnable target.Thread(Runnable target, String name): Creates a named thread with a Runnable target.
class TaskRunnerA implements Runnable {
@Override
public void run() {
for (int idx = 0; idx < 50; idx++) {
System.out.println(Thread.currentThread().getName() + " executing step " + idx);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TaskRunnerB implements Runnable {
@Override
public void run() {
for (int idx = 0; idx < 50; idx++) {
System.out.println(Thread.currentThread().getName() + " performing action " + idx);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
TaskRunnerA runnerA = new TaskRunnerA();
TaskRunnerB runnerB = new TaskRunnerB();
Thread thread1 = new Thread(runnerA);
Thread thread2 = new Thread(runnerB, "CustomThread");
thread1.start();
thread2.start();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " running iteration " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Comparing Thread and Runnable
- Extending
Threadlimits resource sharing since each instance operates in separate memory. - Implementing
Runnablefacilitates resource sharing by allowing multipleThreadobjects to reference the sameRunnableinstance.
Advantages of Runnable
- Enables multiple threads to share common code and resources.
- Avoids Java's single inheritance constraint.
- Compatible with thread pools that require
RunnableorCallableimplementations.
Anonymous Inner Class for Thread Creation
public class AnonymousThreadExample {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("Anonymous thread executing " + i);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
for (int j = 0; j < 50; j++) {
System.out.println(Thread.currentThread().getName() + " processing " + j);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread Lifecycle and States
Threads transition through various states during their lifecycle:
- NEW: Thread object created but not started.
- RUNNABLE: Thread is ready or actively executing.
- BLOCKED: Thread is waiting for a lock or I/O operation.
- WAITING: Thread is indefinitely waiting for another thread's signal.
- TIMED_WAITING: Thread is waiting with a specified timeout.
- TERMINATED: Thread has completed execution or terminated abnormally.
Use Thread.getState() to retrieve the current state.
Main Thread
The main thread is the initial thread that executes the main method. It may spawn child threads and does not necessarily finish last.
Thread Priority
Thread priority ranges from 1 (lowest) to 10 (highest), with a default of 5. Higher priority threads get more CPU time, but lower priority threads can still execute.
setPriority(int priority): Sets thread pirority.getPriority(): Returns current priority.
Thread Control Methods
join()
Causes the calling thread to pause until the joined thread completes.
public static void main(String[] args) throws InterruptedException {
WorkerThreadA worker = new WorkerThreadA();
worker.setName("Worker-A");
worker.start();
worker.join(); // Main thread waits for worker to finish
for (int i = 0; i < 50; i++) {
System.out.println("Main thread resuming " + i);
Thread.sleep(300);
}
}
yield()
Temporarily pauses the current thread to allow other threads to execute, moving it to the runnable state.
Daemon Threads
Daemon threads (e.g., garbage collector) run in the background and terminate automatically when all non-daemon threads finish. Use setDaemon(true) before starting the thread.
WorkerThreadA daemonWorker = new WorkerThreadA();
WorkerThreadB regularWorker = new WorkerThreadB();
daemonWorker.setName("Daemon");
regularWorker.setName("Regular");
daemonWorker.setDaemon(true);
daemonWorker.start();
regularWorker.start();