Back to Java
2026-03-237 min read

Java Threads

Learn Java Threads step by step with clear examples and exercises.

Why This Matters

Welcome to this full guide on Java Threads! Mastering multithreading is crucial in creating high-performance, scalable applications that can handle multiple tasks concurrently within a single JVM. By effectively utilizing threads, developers can improve performance, efficient resource utilization, and deliver smoother user experiences.

In today's world, where multi-core processors are the norm, it is essential to write code that takes advantage of these resources by leveraging concurrent execution. Concurrency allows applications to perform multiple tasks simultaneously, reducing latency and improving responsiveness. Understanding Java threads is vital for developing scalable, high-performance applications, as well as for acing interviews and debugging real-world issues.

Prerequisites

To fully grasp the concepts covered in this lesson, you should have a solid understanding of:

  1. Basic Java syntax and OOP principles (classes, objects, methods, inheritance, polymorphism)
  2. Control structures (loops, conditionals, switch statements)
  3. Exception handling (try-catch blocks, finally block, exceptions hierarchy)
  4. Synchronization and concurrency basics (synchronized keywords, wait(), notify(), join())
  5. Understanding of the Callable and Future interfaces for asynchronous computations
  6. Familiarity with Java's Concurrent API (Executors, BlockingQueue, Atomic classes)
  7. Basic understanding of CPU architecture and how multi-core processors work
  8. Knowledge of common concurrency patterns such as producer-consumer, observer, and strategy patterns

Core Concept

Understanding Threads

In Java, a thread represents a separate path of execution within a program. Each thread runs in its own stack and can execute code concurrently with other threads. The Thread class is the foundation for creating and managing threads in Java.

public class MyThread extends Thread {
public void run() {
// Your code here
}
}

To start a new thread, you create a subclass of Thread, override its run() method, and call the start() method on an instance of your custom thread class.

Creating and Starting Threads (Expanded)

Here's an example of creating and starting a simple thread:

public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // Start the thread
}
}

class MyThread extends Thread {
public void run() {
System.out.println("Hello from a separate thread!");
}
}

When you run this code, the main() method starts first and creates an instance of MyThread. Then it calls start(), which causes the JVM to schedule the execution of the run() method in a separate thread.

Thread States (Expanded)

A Java thread can be in one of the following states:

  1. New: The thread has been created but not yet started.
  2. Runnable: The thread is scheduled to run, but it may not be currently executing.
  3. Running: The thread's run() method is actively being executed by the JVM.
  4. Blocked: The thread is waiting for a specific condition to be met before it can continue execution (e.g., waiting on a lock).
  5. Waiting: The thread is waiting for another object or event to occur, such as waiting for a notification from another thread using the wait() method.
  6. Timed Waiting: Similar to waiting, but with a specified timeout. This state includes methods like sleep(), join(), and various I/O operations that may block the thread temporarily.
  7. Terminated: The thread has completed its execution and is no longer running.

Thread Priority (Expanded)

Thread priority determines the order in which threads are scheduled by the JVM. Higher-priority threads have a better chance of being executed before lower-priority threads when multiple threads are runnable. By default, all threads created have a priority of NORM_PRIORITY (5). You can set the thread's priority using the setPriority() method.

myThread.setPriority(Thread.MAX_PRIORITY); // Set maximum priority for myThread

Thread Synchronization (Expanded)

Synchronization is essential when multiple threads share resources or data, as unsynchronized access can lead to inconsistent results and concurrency issues. Java provides several mechanisms for synchronizing threads:

  1. Synchronized Keywords: You can make a method or block of code thread-safe by using the synchronized keyword. When a thread enters a synchronized method or block, it acquires a lock on the object that contains the method or block. Other threads attempting to enter the same method or block will be blocked until the first thread releases the lock.
public void mySyncedMethod(Object lock) {
synchronized (lock) {
// Your code here
}
}
  1. wait(), notify(), and notifyAll(): These methods are used for cooperative thread synchronization, allowing threads to wait and signal each other when specific conditions are met. The wait(), notify(), and notifyAll() methods should be called within a synchronized block or method.
  1. Lock Interface: The java.util.concurrent.locks.Lock interface provides more flexible synchronization than the synchronized keyword, allowing for fairness, interruption, read-write locks, and other advanced features. Examples include ReentrantLock, ReadWriteLock, and StampedLock.
  1. Atomic Classes: These classes provide thread-safe operations on simple data types like integers, longs, booleans, and references. Examples include AtomicInteger, AtomicLong, and AtomicReference.

Worked Example

In this example, we will create two threads that print their names 10 times each with a 500ms delay between prints using synchronization:

public class ThreadExample {
public static void main(String[] args) {
Thread thread1 = new MyThread("Thread 1");
Thread thread2 = new MyThread("Thread 2");

thread1.start();
thread2.start();

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class MyThread extends Thread {
private final String name;
private int count = 0;

public MyThread(String name) {
this.name = name;
}

@Override
public void run() {
synchronized (this) {
while (count < 10) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + ": " + count++);
}
}
}
}

When you run this code, both threads will print their names concurrently with the specified delay between prints. The join() method is used to ensure that the main thread waits for both custom threads to complete before terminating.

Common Mistakes

1. Forgetting to call start()

Always remember to call start() on your thread instance after creating it, or the thread will never run.

2. Concurrent access to shared resources without synchronization

If multiple threads access and modify a shared resource simultaneously, inconsistent results may occur. Use synchronization mechanisms like synchronized, Lock, or Atomic classes to ensure proper concurrency control.

3. Not handling thread interruption correctly

When you interrupt a running thread with the interrupt() method, it should respond appropriately by checking the interrupted status and exiting gracefully. Failing to do so can cause the thread to become unresponsive or difficult to manage.

4. Mismanaging thread priorities

Setting thread priorities incorrectly can lead to unexpected behavior, as higher-priority threads may preempt lower-priority threads even when they are not ready to run. Be mindful of the impact of thread priority on your application's performance and responsiveness.

5. Not using Executors for thread management

Executors provide a more flexible way to manage threads, allowing you to easily create, schedule, and terminate threads as needed. Examples include Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(), and Executors.newCachedThreadPool().

Practice Questions

  1. Write a Java program that creates three threads, each printing its thread name 10 times with a 500ms delay between prints using synchronization.
  2. Modify the previous example to make the two odd and even threads print their lines in an interleaved fashion, with no delays between prints using synchronized blocks or locks.
  3. Create a Java program that simulates a simple producer-consumer scenario using two threads. The producer generates random integers and stores them in a shared buffer, while the consumer retrieves and processes the integers from the buffer using synchronization.
  4. Write a Java program that uses an ExecutorService to run 10 concurrent tasks that perform a time-consuming operation (e.g., sleeping for a certain amount of time). Measure and compare the performance differences between using a fixed thread pool, single thread executor, and cached thread pool.
  5. Implement a simple chat server using multiple threads to handle client connections and messages concurrently. Use synchronization mechanisms like locks or Atomic classes to ensure proper communication between clients and server components.

FAQ

1. What happens if you call start() on a thread more than once?

Calling start() multiple times on the same thread will result in an exception being thrown, as the thread can only be started once.

2. How do I ensure that my threads run concurrently without interfering with each other?

Use synchronization mechanisms like synchronized, Lock, or Atomic classes to control access to shared resources and prevent conflicts between threads.

3. What is the difference between yield() and sleep() methods in Java threads?

The yield() method allows a running thread to temporarily relinquish its CPU time, giving other runnable threads a chance to execute. The sleep() method causes the current thread to pause execution for a specified amount of time, regardless of whether other threads are ready to run.

4. How do I create a daemon thread?

Daemon threads are background threads that run when there are no user-created non-daemon threads left running. To create a daemon thread, call the setDaemon() method on your thread instance before calling start().

myThread.setDaemon(true); // Set myThread as a daemon thread

5. How do I join threads and wait for their completion?

The join() method allows you to wait for the termination of another thread. When called on a thread, it blocks the calling thread until the specified thread is terminated. You can use the isAlive() method to check if a thread is still running.

myThread.start();
try {
myThread.join(); // Wait for myThread to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
Java Threads | Java | XQA Learn