Fundamentals of Multithreading in Java

Threads and processes are core concepts in concurrent programming. A thread is the smallest unit of execution within a process, leveraging the program's assigned resources. In contrast, a process is an independent unit of resource allocation and scheduling, typically representing a running program that contains at least one thread.

Key distinctions between processes and threads include:

  • A process operates with its own isolated address space, while threads within the same process share the process's address space.
  • Processes are units of resource ownership and allocation; threads share these resources.
  • Threads are the fundamental units of processor scheduling, whereas processes are not.

Both threads and processes progress through five lifecycle states: new, runnable, running, blocked, and terminated.

  • New State: A thread object is created using the new keyword with Thread or a subclass, remaining in this state until its start() method is invoked.
  • Runnable State: After start() is called, the thread enters the runnable state, residing in a queue until scheduled by the JVM's thread scheduler.
  • Running State: A runnable thread that acquires CPU resources begins executing its run() method. From this state, it can transition to blocked, runnable, or terminated states.
  • Blocked State: A running thread may become blocked after invoking methods like sleep() or suspend(), releasing held resources. Blocked states are categorized as:
    • Waiting Block: Triggered by a wait() call.
    • Synchronized Block: Occurs when a thread fails to acquire a synchronized lock.
    • Other Block: Caused by sleep(), join(), or I/O operations. The thread returns to runnable once the blocking condition ends.
  • Terminated State: A thread reaches termination after completing its task or meeting other exit conditions.

Single-threaded programs contain only one sequence of control flow, such as the main method executing as the main thread. Multithreading involves multiple sequential flows running concurrently within a single program. Multithreading is beneficial when multiple subsystems require concurrent execution, but excessive thread creation can degrade performance due to context-switching overhead. Effective multithreading hinges on understanding concurrent, rather than sequential, execution.

Threads can be instantiated by extending the Thread class, implementing the Runnable interface, or implementing the Callable interface, which can return a result via Future. The start() method initiates thread execution, placing it in a runnable state for scheduling. Directly calling run() executes it as a regular method without thread scheduling.

public class ThreadCreationExample {
    public static void main(String[] args) {
        ThreadSubclass t1 = new ThreadSubclass();
        t1.start();

        RunnableImpl r1 = new RunnableImpl();
        Thread t2 = new Thread(r1);
        t2.start();

        CallableImpl c1 = new CallableImpl();
        FutureTask<Integer> task = new FutureTask<>(c1);
        Thread t3 = new Thread(task);
        t3.start();
        try {
            System.out.println("Result: " + task.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class ThreadSubclass extends Thread {
    @Override
    public void run() {
        System.out.println("Thread via Thread extension");
    }
}

class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread via Runnable implementation");
    }
}

class CallableImpl implements Callable<Integer> {
    @Override
    public Integer call() {
        System.out.println("Thread via Callable implementation");
        return 42;
    }
}

Output:

Thread via Thread extension
Thread via Runnable implementation
Thread via Callable implementation
Result: 42

Extending Thread simplifies access to thread methods like sleep() and getId(). However, it limits resource sharing and integration with thread pools due to Java's single inheritance constraint. Implementing Runnable or Callable promotes code reuse, enables resource sharing, avoids inheritance limitations, and works seamlessly with thread pools, though method calls may be more verbose (e.g., Thread.currentThread().getId()). For single-threaded scenarios, extending Thread is often suitable; for multithreaded applications, Runnable or Callable is generally preferred.

Common Thread Methods

  • yield(): Suggests that the current thread is willing to yield its current use of a processor, allowing other threads to run. It transitions the thread back to the runnable state, where it may immediately be rescheduled.
public class YieldDemo {
    public static void main(String[] args) {
        YieldTask task1 = new YieldTask("Alpha");
        YieldTask task2 = new YieldTask("Beta");
        new Thread(task1).start();
        new Thread(task2).start();
    }
}

class YieldTask implements Runnable {
    private final String identifier;
    public YieldTask(String id) { this.identifier = id; }
    @Override
    public void run() {
        System.out.println(identifier + " starting");
        for (int i = 1; i <= 5; i++) {
            System.out.println(identifier + " - " + i);
            if (i == 3) Thread.yield();
        }
        System.out.println(identifier + " ending");
    }
}

Output may vary due to thread scheduling:

Alpha starting
Alpha - 1
Alpha - 2
Alpha - 3
Beta starting
Beta - 1
Beta - 2
Beta - 3
Alpha - 4
Alpha - 5
Alpha ending
Beta - 4
Beta - 5
Beta ending

Note: yield() only suggests relinquishing the CPU; the scheduler may ignore it. Unlike sleep(), which guarantees a minimum pause, yield() does not put the thread into a blocked state.

  • join(): Causes the current thread to wait until the thread on which its called terminates. If called on a thread instance, subsequent code in the calling thread will not execute until the target thread finishes.
public class JoinDemo {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + " begins");
        Worker w1 = new Worker("Worker-A");
        Worker w2 = new Worker("Worker-B");
        w1.start();
        w2.start();
        try {
            w1.join();
            w2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " ends");
    }
}

class Worker extends Thread {
    public Worker(String name) { super(name); }
    @Override
    public void run() {
        System.out.println(getName() + " starts");
        for (int i = 0; i < 3; i++) {
            System.out.println(getName() + " counter: " + i);
            try {
                Thread.sleep(new java.util.Random().nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(getName() + " ends");
    }
}

Sample Output:

main begins
Worker-A starts
Worker-A counter: 0
Worker-B starts
Worker-B counter: 0
Worker-A counter: 1
Worker-B counter: 1
Worker-A counter: 2
Worker-A ends
Worker-B counter: 2
Worker-B ends
main ends
  • setPriority(int): Sets a thread's priority. Thread priorities range from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10), with Thread.NORM_PRIORITY (5) as the default. Priority inheritance occurs when a thread creates another; the child inherits the parent's priority. Note: Priority is a hint to the scheduler and does not guarantee execution order.
public class PriorityDemo {
    public static void main(String[] args) {
        PriorityWorker pw1 = new PriorityWorker("Low-Priority");
        PriorityWorker pw2 = new PriorityWorker("High-Priority");
        pw1.setPriority(Thread.MIN_PRIORITY);
        pw2.setPriority(Thread.MAX_PRIORITY);
        pw1.start();
        pw2.start();
    }
}

class PriorityWorker extends Thread {
    public PriorityWorker(String name) { super(name); }
    @Override
    public void run() {
        System.out.println(getName() + " starts");
        for (int i = 0; i < 3; i++) {
            System.out.println(getName() + " iteration " + i);
            try {
                Thread.sleep(new java.util.Random().nextInt(50));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(getName() + " ends");
    }
}

Output is non-deterministic; the high-priority thread may not always run first.

Summary of Key Thread Methods

  1. sleep(long millis): Temporarily halts execution for the specified duration without releasing held locks.
  2. join(): Waits for the target thread to terminate.
  3. yield(): Hints to the scheduler that the current thread is willing to yield its current time slice.
  4. setPriority(int priority): Attempts to set the thread's scheduling priority.
  5. interrupt(): Interrupts the thread, potentially causing it to throw an InterruptedException if blocked.
  6. wait(): An Object method causing the current thread to wait, releasing the object's monitor. Must be called within a synchronized block and requires exception handling.
  7. isAlive(): Tests if the thread is alive.
  8. activeCount(): Returns an estimate of active threads in the current thread's group.
  9. enumerate(Thread[] tarray): Copies active threads from the current thread's group into an array.
  10. currentThread(): Returns a reference to the currently executing thread.
  11. setDaemon(boolean on): Marks the thread as a daemon (background) thread, which does not prevent the JVM from exiting if only daemon threads remain.
  12. setName(String name): Assigns a name to the thread.
  13. notify() / notifyAll(): Object methods used to wake up threads waiting on the object's monitor, typically used with wait().

Tags: java multithreading Concurrency threads programming

Posted on Fri, 12 Jun 2026 17:45:16 +0000 by HairBomb