Java Multithreading Implementation and Thread Synchronization
Process and Thread Fundamentals
Processes maintain independent memory spaces and code segments, requiring significant overhead for context switching. Each process can contain multiple threads. Threads within the same process share memory and code space but maintain separate execution stacks and program counters, enabling lightweight context switching.
Both processes and threads follow five lifecycle stages: creation, ready, running, blocked, and termination. Multiprocessing enables concurrent execution of multiple applications, while multithreading allows multiple sequential flows within a single program.
Creating Threads in Java
Java provides two primary approaches for thread creation: extending the Thread class or implementing the Runnable interface.
Extending Thread Class
class WorkerThread extends Thread {
private String identifier;
public WorkerThread(String id) {
this.identifier = id;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(identifier + " executing : " + i);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Application {
public static void main(String[] args) {
WorkerThread threadA = new WorkerThread("X");
WorkerThread threadB = new WorkerThread("Y");
threadA.start();
threadB.start();
}
}
The start() method transitions threads to runnable state, with actual execution timing determined by the operating system scheduler. Thread.sleep() yields CPU resources to other threads during execusion.
Implementing Runnable Interface
class TaskRunner implements Runnable {
private String label;
public TaskRunner(String label) {
this.label = label;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(label + " executing : " + i);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Executor {
public static void main(String[] args) {
new Thread(new TaskRunner("M")).start();
new Thread(new TaskRunner("N")).start();
}
}
Thread vs Runnable Comparison
Inheritance-based threading creates isolated resource instances, while Runnable implementation anables resource sharing across threads.
class SharedCounter implements Runnable {
private int sharedValue = 15;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +
" processing value= " + sharedValue--);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ResourceCoordinator {
public static void main(String[] args) {
SharedCounter counter = new SharedCounter();
new Thread(counter, "Alpha").start();
new Thread(counter, "Beta").start();
new Thread(counter, "Gamma").start();
}
}
Runnable implementation offers advantages including resource sharing capabilities, avoidance of Java's single inheritance limitation, and enhanced code modularity.
Thread Lifecycle States
- New: Thread instance created
- Runnable: Thread eligible for CPU execution after start() invocation
- Running: Thread actively executing code
- Blocked: Thread suspended due to various conditions:
- Wait blocking: Thread in waiting pool after wait() call
- Synchronization blocking: Thread in lock pool awaiting monitor acquisition
- General blocking: Thread suspended during sleep(), join(), or I/O operations
- Terminated: Thread completed execution or exited via exception
Thread Scheduling Mechanisms
Priority Management
Thread priorities range from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with default priority set to 5 (NORM_PRIORITY). Priority inheritance occurs when threads spawn child threads.
Thread Control Methods
- sleep(long millis): Temporarily suspends thread execution
- wait(): Places thread in waiting state until notified
- yield(): Voluntarily yields CPU to threads of equal or higher priority
- join(): Suspends calling thread until target thread completes
- notify()/notifyAll(): Wakes waiting threads on object monitor
Join Method Implementation
class ComputationThread extends Thread {
private String name;
public ComputationThread(String name) {
super(name);
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started computation");
for (int i = 0; i < 5; i++) {
System.out.println("Processing " + name + " : " + i);
try {
sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " completed");
}
}
class Coordinator {
public static void main(String[] args) {
System.out.println("Main thread initiating");
ComputationThread worker1 = new ComputationThread("A");
ComputationThread worker2 = new ComputationThread("B");
worker1.start();
worker2.start();
try {
worker1.join();
worker2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread terminating");
}
}
Synchronization Primitives
Wait/Notify Coordination
class SequentialPrinter implements Runnable {
private String identifier;
private Object previous;
private Object current;
public SequentialPrinter(String id, Object prev, Object curr) {
this.identifier = id;
this.previous = prev;
this.current = curr;
}
@Override
public void run() {
int iterations = 10;
while (iterations > 0) {
synchronized (previous) {
synchronized (current) {
System.out.print(identifier);
iterations--;
current.notify();
}
try {
previous.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class SynchronizationDemo {
public static void main(String[] args) throws Exception {
Object lockA = new Object();
Object lockB = new Object();
Object lockC = new Object();
SequentialPrinter printerA = new SequentialPrinter("A", lockC, lockA);
SequentialPrinter printerB = new SequentialPrinter("B", lockA, lockB);
SequentialPrinter printerC = new SequentialPrinter("C", lockB, lockC);
new Thread(printerA).start();
Thread.sleep(100);
new Thread(printerB).start();
Thread.sleep(100);
new Thread(printerC).start();
Thread.sleep(100);
}
}
Data Exchange Between Threads
Constructor-based Data Transfer
class ParameterizedThread extends Thread {
private String message;
public ParameterizedThread(String msg) {
this.message = msg;
}
@Override
public void run() {
System.out.println("Greetings " + message);
}
}
class ThreadLauncher {
public static void main(String[] args) {
Thread worker = new ParameterizedThread("universe");
worker.start();
}
}
Method-based Data Assignment
class ConfigurableTask implements Runnable {
private String message;
public void setMessage(String msg) {
this.message = msg;
}
@Override
public void run() {
System.out.println("Greetings " + message);
}
}
class TaskExecutor {
public static void main(String[] args) {
ConfigurableTask task = new ConfigurableTask();
task.setMessage("universe");
Thread worker = new Thread(task);
worker.start();
}
}
Callback Pattern for Dynamic Data
class ResultContainer {
public int sum = 0;
}
class Calculator {
public void computeSum(ResultContainer result, int... numbers) {
for (int num : numbers) {
result.sum += num;
}
}
}
class ComputationalThread extends Thread {
private Calculator processor;
public ComputationalThread(Calculator calc) {
this.processor = calc;
}
@Override
public void run() {
java.util.Random generator = new java.util.Random();
ResultContainer outcome = new ResultContainer();
int val1 = generator.nextInt(1000);
int val2 = generator.nextInt(2000);
int val3 = generator.nextInt(3000);
processor.computeSum(outcome, val1, val2, val3);
System.out.println(val1 + "+" + val2 + "+" + val3 + "=" + outcome.sum);
}
}
class CallbackDemo {
public static void main(String[] args) {
Thread computation = new ComputationalThread(new Calculator());
computation.start();
}
}