Java Thread Basics
Introduction
Multithreading is a powerful feature in Java that allows applications to perform multiple operations concurrently. At the core of Java's multithreading capabilities is the concept of a thread - an independent path of execution within a program. Threads allow developers to build applications that can perform multiple tasks simultaneously, making better use of available CPU resources and improving application responsiveness.
In this tutorial, we'll cover the fundamentals of Java threads, including:
- What threads are and why they're important
- Different ways to create threads in Java
- Understanding thread lifecycle
- Basic thread operations
- Common use cases and practical examples
By the end of this guide, you'll have a solid understanding of Java thread basics and be ready to write your first multithreaded applications.
What is a Thread?
A thread is the smallest unit of execution within a process. When you run a Java program, the Java Virtual Machine (JVM) starts a single thread called the "main thread" that executes your main()
method. However, you can create additional threads to perform tasks concurrently with the main thread.
Why Use Threads?
There are several compelling reasons to use threads in your applications:
- Improved Responsiveness: In GUI applications, background tasks can run in separate threads without freezing the user interface
- Increased Performance: On multi-core CPUs, threads can execute in parallel, reducing processing time
- Resource Efficiency: Threads share the same memory space, making them more lightweight than separate processes
- Simplified Program Structure: Some problems are naturally modeled as concurrent activities
Creating Threads in Java
Java provides two primary ways to create threads:
- By extending the
Thread
class - By implementing the
Runnable
interface
Let's examine both approaches:
Method 1: Extending the Thread Class
This approach involves creating a subclass of Thread
and overriding its run()
method with your code:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
// Your thread code goes here
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500); // Pausing execution for 500 milliseconds
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}
}
To use this thread:
public class ThreadExample {
public static void main(String[] args) {
System.out.println("Main thread starting...");
// Creating thread objects
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// Setting names for easier identification
thread1.setName("Thread-1");
thread2.setName("Thread-2");
// Starting the threads
thread1.start();
thread2.start();
System.out.println("Main thread continues executing...");
}
}
Output:
Main thread starting...
Main thread continues executing...
Thread is running: Thread-1
Thread is running: Thread-2
Thread-1: 0
Thread-2: 0
Thread-1: 1
Thread-2: 1
Thread-1: 2
Thread-2: 2
Thread-1: 3
Thread-2: 3
Thread-1: 4
Thread-2: 4
Note: The exact order of thread execution may vary between runs, which is an important characteristic of multithreaded programs.
Method 2: Implementing the Runnable Interface
This is the preferred approach as it separates the thread's tasks from its mechanics and doesn't consume your single inheritance opportunity:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
// Your thread code goes here
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}
}
To use this runnable:
public class RunnableExample {
public static void main(String[] args) {
System.out.println("Main thread starting...");
// Creating Runnable instance
MyRunnable runnable = new MyRunnable();
// Creating Thread objects with the Runnable
Thread thread1 = new Thread(runnable, "Thread-1");
Thread thread2 = new Thread(runnable, "Thread-2");
// Starting the threads
thread1.start();
thread2.start();
System.out.println("Main thread continues executing...");
}
}
Output:
Main thread starting...
Main thread continues executing...
Thread is running: Thread-1
Thread is running: Thread-2
Thread-1: 0
Thread-2: 0
Thread-2: 1
Thread-1: 1
Thread-1: 2
Thread-2: 2
Thread-2: 3
Thread-1: 3
Thread-1: 4
Thread-2: 4
Method 3: Using Lambda Expressions (Java 8+)
In modern Java, you can create threads more concisely using lambda expressions:
public class LambdaThreadExample {
public static void main(String[] args) {
System.out.println("Main thread starting...");
// Creating a thread with lambda expression
Thread thread1 = new Thread(() -> {
System.out.println("Thread is running: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}, "Lambda-Thread");
// Start the thread
thread1.start();
System.out.println("Main thread continues executing...");
}
}
Output:
Main thread starting...
Main thread continues executing...
Thread is running: Lambda-Thread
Lambda-Thread: 0
Lambda-Thread: 1
Lambda-Thread: 2
Lambda-Thread: 3
Lambda-Thread: 4
Thread Lifecycle
A Java thread goes through various states throughout its lifetime. Understanding these states is crucial for effective thread management.
- New: The thread has been created but not yet started.
- Runnable: The thread is ready to run and waiting for CPU allocation.
- Running: The thread is currently executing.
- Blocked/Waiting: The thread is temporarily inactive (sleeping, waiting for a resource, etc.).
- Terminated: The thread has completed execution or was stopped.
You can check a thread's state using the getState()
method:
Thread thread = new Thread(() -> {
// Thread code
});
System.out.println("After creation: " + thread.getState()); // NEW
thread.start();
System.out.println("After start: " + thread.getState()); // RUNNABLE
Basic Thread Operations
Starting a Thread
To start a thread, call its start()
method. This tells the JVM to allocate resources and execute the thread's run()
method:
Thread thread = new Thread(() -> System.out.println("Thread running"));
thread.start(); // Don't call run() directly!
⚠️ Important: Never call a thread's
run()
method directly. This will execute the code in the current thread, not in a new thread!
Sleeping a Thread
To pause a thread's execution temporarily, use the Thread.sleep()
method:
try {
// Sleep for 2 seconds
Thread.sleep(2000);
} catch (InterruptedException e) {
// Handle interruption
}
Joining Threads
Sometimes you need to wait for a thread to complete before proceeding. The join()
method allows one thread to wait for another to finish:
public class JoinExample {
public static void main(String[] args) {
Thread workerThread = new Thread(() -> {
System.out.println("Worker thread started");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker thread finished");
});
System.out.println("Main thread: Starting worker");
workerThread.start();
try {
// Main thread will wait for workerThread to finish
workerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread: Worker has completed its task");
}
}
Output:
Main thread: Starting worker
Worker thread started
Worker thread finished
Main thread: Worker has completed its task
Thread Priority
Java allows you to influence thread scheduling by setting priorities. Thread priorities range from 1 (lowest) to 10 (highest), with 5 being the default:
Thread thread = new Thread(() -> {
// Code here
});
thread.setPriority(Thread.MAX_PRIORITY); // 10
thread.setPriority(Thread.MIN_PRIORITY); // 1
thread.setPriority(Thread.NORM_PRIORITY); // 5
Note: Thread priorities are just hints to the operating system and don't guarantee execution order. Their effect varies across different operating systems and JVM implementations.
Practical Examples
Example 1: Simple Download Simulator
Let's build a simple file download simulator to demonstrate how threads can improve user experience:
public class DownloadSimulator {
public static void main(String[] args) {
System.out.println("Download Manager Starting...");
// Without threads (sequential downloads)
long startTime = System.currentTimeMillis();
downloadFile("Document1.pdf");
downloadFile("Image1.jpg");
downloadFile("Video1.mp4");
long endTime = System.currentTimeMillis();
System.out.println("Sequential downloads took: " + (endTime - startTime) + "ms");
System.out.println("\nNow trying with threads (parallel downloads)...\n");
// With threads (parallel downloads)
Thread thread1 = new Thread(() -> downloadFile("Document1.pdf"));
Thread thread2 = new Thread(() -> downloadFile("Image1.jpg"));
Thread thread3 = new Thread(() -> downloadFile("Video1.mp4"));
startTime = System.currentTimeMillis();
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
endTime = System.currentTimeMillis();
System.out.println("Parallel downloads took: " + (endTime - startTime) + "ms");
}
private static void downloadFile(String fileName) {
System.out.println("Started downloading: " + fileName);
// Simulate file download time
try {
Thread.sleep(2000); // 2 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Finished downloading: " + fileName);
}
}
Output:
Download Manager Starting...
Started downloading: Document1.pdf
Finished downloading: Document1.pdf
Started downloading: Image1.jpg
Finished downloading: Image1.jpg
Started downloading: Video1.mp4
Finished downloading: Video1.mp4
Sequential downloads took: 6023ms
Now trying with threads (parallel downloads)...
Started downloading: Document1.pdf
Started downloading: Image1.jpg
Started downloading: Video1.mp4
Finished downloading: Document1.pdf
Finished downloading: Image1.jpg
Finished downloading: Video1.mp4
Parallel downloads took: 2011ms
This example clearly shows how using threads can significantly improve performance for I/O-bound tasks by executing them in parallel.
Example 2: Real-time Progress Updates
Let's create a scenario where a background thread performs a task while the main thread can continue to respond to the user:
public class ProgressReporter {
private static boolean taskComplete = false;
public static void main(String[] args) {
System.out.println("Application started");
// Start the long-running task in a background thread
Thread backgroundTask = new Thread(() -> {
System.out.println("Background task started");
try {
// Simulate a long-running operation (10 seconds)
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
System.out.println("Task progress: " + (i * 10) + "% complete");
}
} catch (InterruptedException e) {
System.out.println("Background task interrupted");
return;
}
taskComplete = true;
System.out.println("Background task completed!");
});
// Make this a daemon thread that won't prevent the JVM from exiting
// backgroundTask.setDaemon(true);
// Start the background task
backgroundTask.start();
// Main thread continues to do other work
System.out.println("Main thread continues to run...");
// Simulate the main thread doing quick tasks while waiting
for (int i = 0; i < 5; i++) {
System.out.println("Main thread: Processing user input " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Main thread waits for background task to complete
System.out.println("Main thread: Waiting for background task to complete...");
try {
backgroundTask.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (taskComplete) {
System.out.println("Main thread: Background task is now complete!");
}
System.out.println("Application ending");
}
}
Output:
Application started
Background task started
Main thread continues to run...
Main thread: Processing user input 0
Task progress: 10% complete
Main thread: Processing user input 1
Main thread: Processing user input 2
Task progress: 20% complete
Main thread: Processing user input 3
Main thread: Processing user input 4
Task progress: 30% complete
Main thread: Waiting for background task to complete...
Task progress: 40% complete
Task progress: 50% complete
Task progress: 60% complete
Task progress: 70% complete
Task progress: 80% complete
Task progress: 90% complete
Task progress: 100% complete
Background task completed!
Main thread: Background task is now complete!
Application ending
This example demonstrates how threads allow an application to perform long-running tasks while still remaining responsive to users.
Best Practices and Common Pitfalls
When working with threads, keep these best practices in mind:
- Prefer
Runnable
over extendingThread
: This follows good OOP principles and preserves your class's inheritance capabilities - Always handle
InterruptedException
: This allows your threads to respond properly to cancellation requests - Use meaningful thread names: This makes debugging much easier
- Be careful with shared resources: Threads that modify the same data can cause concurrency issues
- Don't call
Thread.run()
directly: Always useThread.start()
to launch a new thread - Consider using higher-level concurrency utilities: For most applications, the classes in
java.util.concurrent
provide safer and more convenient alternatives
Summary
In this tutorial, you've learned about the fundamentals of Java threads:
- Threads are independent paths of execution within a Java program
- You can create threads by extending the
Thread
class, implementing theRunnable
interface, or using lambda expressions - Threads go through distinct lifecycle states: New, Runnable, Running, Blocked/Waiting, and Terminated
- Basic operations include starting threads, making them sleep, and joining them
- Threads can significantly improve application performance and responsiveness
Understanding threads is essential for developing responsive, efficient Java applications, especially for programs that involve I/O operations, user interfaces, or need to leverage multi-core processors.
Additional Resources
To deepen your understanding of Java multithreading:
- Java Documentation: Oracle's official tutorial on concurrency
- Books: "Java Concurrency in Practice" by Brian Goetz is highly recommended
- Advanced Topics: Once you're comfortable with thread basics, explore thread pools, synchronization, locks, and the concurrency utilities in the
java.util.concurrent
package
Exercises
To practice your understanding of threads:
- Create a simple counter application where two threads increment a shared counter and print its value
- Modify the download simulator to handle an arbitrary number of files with a configurable maximum number of concurrent downloads
- Create a program that simulates a race between multiple threads, each representing a different runner
- Build a simple chat application where one thread listens for incoming messages while another sends outgoing messages
- Implement a program that uses threads to find the minimum, maximum, average, and sum of elements in a large array by dividing the work among multiple threads
Happy threading!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)