Skip to content

Java Multithreading

Java is inherently a multithreaded programming language, allowing you to develop programs where multiple tasks run concurrently. A multithreaded program contains two or more parts (threads) that can execute seemingly simultaneously, making optimal use of available resources, especially on multi-core processors.

Multitasking vs. Multithreading: Multitasking involves sharing CPU resources among multiple processes (independent programs). Multithreading extends this concept within a single application, subdividing its operations into individual threads that share the same memory space. Each thread can run in parallel (on multiple cores) or concurrently (interleaved on a single core).

Concurrency enables writing responsive applications where long-running tasks don’t block the main execution flow (e.g., keeping a UI responsive while performing background calculations).

A thread progresses through several states during its lifetime:

  • NEW: The thread has been created but has not yet started execution (i.e., its start() method hasn’t been called).
  • RUNNABLE: The thread is eligible to be run by the thread scheduler. This state includes both threads actually running on the CPU and those ready to run but waiting for the scheduler to allocate CPU time.
  • BLOCKED: The thread is waiting to acquire a monitor lock (e.g., entering a synchronized block or method currently held by another thread).
  • WAITING: The thread is waiting indefinitely for another thread to perform a specific action. This occurs after calls like Object.wait(), Thread.join(), or LockSupport.park().
  • TIMED_WAITING: The thread is waiting for a specified period for another thread to act or for the time to elapse. This occurs after calls like Thread.sleep(long millis), Object.wait(long timeout), Thread.join(long millis), LockSupport.parkNanos(), or LockSupport.parkUntil().
  • TERMINATED: The thread has completed its execution (its run() method has finished) or has otherwise terminated.

Every Java thread has a priority (an integer between Thread.MIN_PRIORITY (1) and Thread.MAX_PRIORITY (10), with Thread.NORM_PRIORITY (5) as the default).

In theory, higher-priority threads should get preference from the scheduler. However, the actual behavior of thread priorities is highly dependent on the underlying operating system and JVM implementation. Relying on priorities for program correctness is not recommended and can lead to non-portable behavior or issues like starvation. Use proper synchronization mechanisms instead.

There are two fundamental ways to define the code that a thread will execute:

1. Implementing the Runnable Interface (Preferred)

Section titled “1. Implementing the Runnable Interface (Preferred)”

This is generally preferred as it separates the task (Runnable) from the execution mechanism (Thread) and allows your class to extend another class if needed.

  1. Create a class that implements the java.lang.Runnable interface.
  2. Implement the public void run() method within that class. This method contains the code the thread will execute.
  3. Create an instance of your Runnable class.
  4. Create an instance of java.lang.Thread, passing your Runnable object to its constructor.
  5. Call the start() method on the Thread object. This creates a new system thread and invokes the run() method of your Runnable object in that new thread.
import java.util.concurrent.TimeUnit;
class MyRunnableTask implements Runnable {
private final String taskName;
public MyRunnableTask(String name) {
this.taskName = name;
System.out.println("Creating task: " + taskName);
}
@Override
public void run() {
System.out.println("Running task: " + taskName);
try {
for (int i = 3; i > 0; i--) {
System.out.println("Task " + taskName + ", Count: " + i);
// Use TimeUnit for clearer sleep duration
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
System.out.println("Task " + taskName + " interrupted.");
}
System.out.println("Task " + taskName + " finished.");
}
}
public class RunnableTest {
public static void main(String[] args) {
System.out.println("Main thread started.");
MyRunnableTask task1 = new MyRunnableTask("Task-A");
Thread thread1 = new Thread(task1);
MyRunnableTask task2 = new MyRunnableTask("Task-B");
Thread thread2 = new Thread(task2);
// Start the threads
thread1.start();
thread2.start();
System.out.println("Main thread finished starting tasks.");
// Note: main thread might finish before task threads complete
}
}

This approach is less flexible because Java only allows single inheritance.

  1. Create a class that extends java.lang.Thread.
  2. Override the public void run() method in your class.
  3. Create an instance of your custom Thread subclass.
  4. Call the start() method on the instance. This performs the same action as in the Runnable case.
import java.util.concurrent.TimeUnit;
class MyThread extends Thread {
public MyThread(String name) {
super(name); // Pass name to Thread constructor
System.out.println("Creating thread: " + getName());
}
@Override
public void run() {
System.out.println("Running thread: " + getName());
try {
for (int i = 3; i > 0; i--) {
System.out.println("Thread " + getName() + ", Count: " + i);
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread " + getName() + " interrupted.");
}
System.out.println("Thread " + getName() + " finished.");
}
}
public class ThreadExtensionTest {
public static void main(String[] args) {
System.out.println("Main thread started.");
MyThread threadA = new MyThread("Thread-A");
MyThread threadB = new MyThread("Thread-B");
threadA.start();
threadB.start();
System.out.println("Main thread finished starting threads.");
}
}

Modern Concurrency: The java.util.concurrent Package

Section titled “Modern Concurrency: The java.util.concurrent Package”

While directly managing Thread objects is fundamental, modern Java development heavily relies on the higher-level abstractions provided in the java.util.concurrent package (introduced in Java 5). This package offers more robust, efficient, and easier-to-use concurrency utilities.

  • Executor Framework: Decouples task submission from task execution. ExecutorService is the primary interface, often created using factory methods in the Executors class (e.g., Executors.newFixedThreadPool(), Executors.newCachedThreadPool()). This is the recommended way to manage threads in most applications.
  • Callable and Future: Callable is similar to Runnable but can return a result and throw checked exceptions. When submitted to an ExecutorService, it returns a Future object, which represents the result of an asynchronous computation.
  • Concurrent Collections: Thread-safe collection implementations like ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue.
  • Synchronizers: Utilities for coordinating threads, such as Semaphore, CountDownLatch, CyclicBarrier, Phaser.
  • Locks: More flexible locking mechanisms than the intrinsic synchronized keyword, like ReentrantLock, ReadWriteLock.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceTest {
public static void main(String[] args) {
System.out.println("Main thread started.");
// Create a fixed-size thread pool
ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit Runnable tasks
executor.submit(new MyRunnableTask("PoolTask-1"));
executor.submit(new MyRunnableTask("PoolTask-2"));
executor.submit(new MyRunnableTask("PoolTask-3")); // Will wait if pool is busy
// Initiate graceful shutdown
executor.shutdown(); // Stops accepting new tasks, waits for existing ones
try {
// Wait for tasks to complete for a maximum time
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Tasks did not complete in time, forcing shutdown.");
executor.shutdownNow(); // Attempt to interrupt running tasks
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("Main thread finished.");
}
// Reusing MyRunnableTask from the previous example
static class MyRunnableTask implements Runnable {
// ... (same implementation as before) ...
private final String taskName;
public MyRunnableTask(String name) {
this.taskName = name;
System.out.println("Creating task: " + taskName);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " running task: " + taskName);
try {
for (int i = 3; i > 0; i--) {
System.out.println("Task " + taskName + ", Count: " + i);
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Task " + taskName + " interrupted.");
}
System.out.println(Thread.currentThread().getName() + " finished task: " + taskName);
}
}
}

Key methods available in the Thread class:

SNMethodDescription
1public void start()Starts the thread’s execution by calling its run() method in a new thread context.
2public void run()The entry point for the thread’s execution logic. Directly calling run() executes the code in the current thread, not a new one.
3public final void join()
public final void join(long millis)
Waits for this thread to terminate. The calling thread blocks until the target thread dies or the timeout occurs.
4public void interrupt()Interrupts this thread. Sets the thread’s interrupt status flag. If the thread is blocked in sleep(), wait(), or join(), it throws an InterruptedException.
5public static boolean interrupted()Checks if the current thread has been interrupted and clears the interrupted status.
6public boolean isInterrupted()Checks if this thread has been interrupted without clearing the status.
7public final boolean isAlive()Returns true if the thread has started and has not yet died.
8public static void sleep(long millis)Causes the current thread to pause execution for the specified milliseconds (subject to scheduler precision). Throws InterruptedException.
9public static Thread currentThread()Returns a reference to the currently executing thread object.
10public final void setDaemon(boolean on)Marks this thread as a daemon thread. The JVM exits when only daemon threads are running.

When multiple threads access shared, mutable data, you need mechanisms to prevent race conditions and ensure data consistency. This is known as thread safety.

Key concepts include:

  • Synchronization: Using mechanisms like the synchronized keyword or java.util.concurrent.locks.Lock implementations to ensure only one thread can access a critical section of code or shared resource at a time.
  • Atomicity: Ensuring an operation completes entirely or not at all, without interference from other threads (e.g., using classes from java.util.concurrent.atomic).
  • Visibility: Ensuring changes made to shared variables by one thread are visible to other threads (using volatile keyword or synchronization).
  • Deadlock: A situation where two or more threads are blocked forever, each waiting for the other to release a resource.
  • Immutable Objects: Objects whose state cannot be changed after creation are inherently thread-safe.

Developing correct concurrent applications requires careful design and understanding of these concepts. Using the utilities from java.util.concurrent often simplifies achieving thread safety compared to manual locking with synchronized.