Java Multithreading Fundamentals and Practical Implementation

Multithreading

1. Concepts: Process and Thread

A process refers to an actively executing program instance. A thread acts as a single execution path within a process, handling the sequential execution of code segments. All application code runs within threads; by default, a JVM launches a main thread to start execution.

Single-threaded execution processes tasks sequentially and cannot handle multiple operations simultaneously. Multithreading enables concurrent (apparent simultaneous) task handling. From a macroscopic perspective, fast thread scheduling creates the illusion of continuous parallel work. Microscopically, threads compete for CPU time slices, with only one thread executing per core at any given moment.

2. Multithreading Creation Approaches

The Thread class and its subclasses represent thread objects in Java. There are two primary creation methods.

Method 1: Extend Thread Class
  1. Define a subclass of Thread and override its run() method to encapsulate thread-specific logic.
  2. Instantiate the subclass, optionally set a thread name, and invoke start() (not run() directly) to launch it.
public class TicketSellerThread extends Thread {
    @Override
    public void run() {
        // Thread execution logic
        for (int count = 0; count < 100; count++) {
            System.out.println("Processing ticket sale step...");
        }
    }
}

Usage example:

public class TicketStation {
    public static void main(String[] args) {
        TicketSellerThread seller1 = new TicketSellerThread();
        TicketSellerThread seller2 = new TicketSellerThread();
        TicketSellerThread seller3 = new TicketSellerThread();
        seller1.setName("Counter A");
        seller2.setName("Counter B");
        seller3.setName("Counter C");
        seller1.start();
        seller2.start();
        seller3.start();
    }
}
Method 2: Implement Runnable Interface
  1. Create a class that implements the Runnable interface and defines the run() method.
  2. Pass an instance of this implementation to one or more Thread constructors.
public class TicketProcessor implements Runnable {
    @Override
    public void run() {
        for (int idx = 0; idx < 100; idx++) {
            System.out.println(Thread.currentThread().getName() + " is handling a ticket request");
        }
    }
}

Usage example:

public class OnlineTicketSystem {
    public static void main(String[] args) {
        TicketProcessor processor = new TicketProcessor();
        Thread worker1 = new Thread(processor, "Server Node 1");
        Thread worker2 = new Thread(processor, "Server Node 2");
        Thread worker3 = new Thread(processor, "Server Node 3");
        worker1.start();
        worker2.start();
        worker3.start();
    }
}

3. Thread Lifecycle States

  1. New: A thread object created via new but not yet started; it cannot compete for CPU resources.
  2. Runnable: After calling start(), the thread enters this state, eligible to grab CPU time slices but not currently executing.
  3. Running: The thread has secured CPU control and is executing its run() method; it returns to Runnable if CPU time expires or it is preempted.
  4. Blocked/Waiting/Timed Waiting: The thread pauses execution due to operations like sleep(), wait(), or I/O; it transitions back to Runnable after the blocking condition resolves (e.g., timeout, notify()).
  5. Terminated: The thread completes run() execution or is interrupted unexpectedly, ending its lifecycle.

4. Common Thread Operations

(1) Adjust Thread Priority

Thread priority ranges from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with a default of 5 (NORM_PRIORITY). Higher priority threads have a greater probability of being scheduled first, not guaranteed.

public class PriorityDemoThread extends Thread {
    @Override
    public void run() {
        for (int step = 0; step < 100; step++) {
            System.out.println(Thread.currentThread().getName() + " is processing step " + step);
        }
    }
}
public class PriorityTest {
    public static void main(String[] args) {
        PriorityDemoThread high = new PriorityDemoThread();
        PriorityDemoThread normal = new PriorityDemoThread();
        PriorityDemoThread low = new PriorityDemoThread();
        high.setName("High-Priority Worker");
        normal.setName("Normal-Priority Worker");
        low.setName("Low-Priority Worker");
        high.setPriority(Thread.MAX_PRIORITY);
        normal.setPriority(Thread.NORM_PRIORITY);
        low.setPriority(Thread.MIN_PRIORITY);
        high.start();
        normal.start();
        low.start();
    }
}
(2) Thread Sleep

Thread.sleep(long millis) pauses the current thread for a specified duration (milliseconds), entering Timed Waiting state, and automatically resumes afterward. It throws InterruptedException.

public class SleepDemoThread extends Thread {
    @Override
    public void run() {
        String threadLabel = Thread.currentThread().getName();
        for (int tick = 0; tick < 100; tick++) {
            if ("Delayed Worker".equals(threadLabel)) {
                try {
                    System.out.println("Delayed Worker pausing for 300ms");
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println(threadLabel + " completed tick " + tick);
        }
    }
}
(3) Thread Yield

Thread.yield() voluntarily releases the current CPU time slice, moving the thread back to Runnable state, allowing other same/higher-priority threads to execute. It does not guarantee immediate handoff.

public class YieldDemoThread extends Thread {
    @Override
    public void run() {
        String id = Thread.currentThread().getName();
        for (int round = 0; round < 100; round++) {
            if ("Yielding Thread".equals(id) && round == 15) {
                Thread.yield();
            }
            System.out.println(id + " is at round " + round);
        }
    }
}
(4) Thread Join

join() forces the current thread to wait until the target thread finishes execution. It throws InterruptedException.

public class ParentTask extends Thread {
    @Override
    public void run() {
        String parentName = Thread.currentThread().getName();
        System.out.println(parentName + " starts preparing dough");
        System.out.println(parentName + " shapes dough into pizza base");
        System.out.println(parentName + " realizes no cheese is left");
        
        ChildTask fetchCheese = new ChildTask();
        fetchCheese.setName("Kid Runner");
        fetchCheese.start();
        
        try {
            fetchCheese.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println(parentName + " adds cheese to pizza base");
        System.out.println(parentName + " bakes and serves pizza");
    }
}
public class ChildTask extends Thread {
    @Override
    public void run() {
        String childName = Thread.currentThread().getName();
        System.out.println(childName + " runs downstairs");
        System.out.println(childName + " walks to nearby grocery");
        System.out.println(childName + " buys mozzarella cheese");
        System.out.println(childName + " returns home with cheese");
    }
}
public class PizzaPrep {
    public static void main(String[] args) {
        ParentTask mom = new ParentTask();
        mom.setName("Mom");
        mom.start();
    }
}

5. Thread Synchronization

Thread safety issues arise when multiple threads modify shared mutable data without coordination, leading to inconsistent results.

Synchronized Code Block

Wrap shared mutable operations in a synchronized block with a common lock object to ensure exclusive execution. Only one thread can enter the block at a time.

public class SharedTicketSeller implements Runnable {
    private static int remainingTickets = 100;
    
    @Override
    public void run() {
        while (true) {
            synchronized (SharedTicketSeller.class) {
                if (remainingTickets <= 0) {
                    break;
                }
                String counter = Thread.currentThread().getName();
                System.out.println(counter + " sold ticket #" + remainingTickets);
                remainingTickets--;
            }
        }
    }
}
Synchronized Methods
Non-Static Synchronized Method

Uses this as the implicit lock object, ensuring exclusive execution per instance.

public synchronized void deductTicket() {
    // Shared mutable logic here
}
Static Synchronized Method

Uses the class’s bytecode object (Class<T>) as the implicit lock, ensuring exclusive execution across all instances.

public static synchronized void resetTicketPool() {
    // Static shared mutable logic here
}

Tags: java multithreading Concurrency Thread Synchronization Thread Lifecycle

Posted on Wed, 13 May 2026 03:08:25 +0000 by Xajel