Java Threads: A Complete Guide with Examples and Exercises
In modern computing, multitasking is essential for building efficient and responsive applications. Java Threads provide a powerful mechanism for concurrent execution, allowing multiple tasks to be performed simultaneously. By leveraging threads, Java programs can improve performance, responsiveness, and resource utilization.
In this article, we will explore how to create and manage threads in Java, the different ways to work with them, and how they are used to achieve multithreading. We will also provide exercises to help you get hands-on experience with Java threads.
Table of Contents
- Introduction to Java Threads
- What is a Thread?
- Creating Threads in Java
- Thread Lifecycle
- Thread Synchronization
- Thread Communication
- Exercise
- Conclusion
1. Introduction to Java Threads
A thread is the smallest unit of a CPU’s execution. Each thread represents a single sequence of instructions that can be executed independently of others. In Java, multithreading allows the execution of two or more threads concurrently, making it possible to perform multiple tasks in parallel.
Java provides built-in support for multithreading through the Thread
class and the Runnable
interface. By creating and managing threads, you can ensure that your program can handle complex tasks such as user input, networking, and file I/O while still remaining responsive.
2. What is a Thread?
A thread is essentially a lightweight process. A Java program can contain one or more threads, with each thread running in parallel. Threads are managed by the Java Virtual Machine (JVM), and each thread has its own execution context, which includes:
- Program counter (PC): Tracks the current instruction being executed.
- Stack: Stores method calls and local variables.
- Register set: Holds temporary data.
- Heap: Shared memory space where all threads can store and access objects.
The main thread in Java is the one that executes the main()
method when the program starts.
3. Creating Threads in Java
There are two main ways to create a thread in Java:
1. Extending the Thread
Class
By extending the Thread
class, you can override the run()
method to define the code that will be executed by the thread. Once the thread is created, it can be started using the start()
method.
Example: Extending the Thread
class
class MyThread extends Thread {
public void run() {
System.out.println("This is a thread running.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread
}
}
In this example, we create a new class MyThread
that extends the Thread
class. The run()
method contains the code that will be executed when the thread starts.
2. Implementing the Runnable
Interface
Another way to create a thread is by implementing the Runnable
interface. This method is preferred when you want to create a thread from a class that already extends another class (since Java supports single inheritance only).
Example: Implementing the Runnable
interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("This is a thread running.");
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable); // Create a thread
thread.start(); // Start the thread
}
}
In this example, we define a MyRunnable
class that implements the Runnable
interface. We create a Thread
object and pass the Runnable
object to the Thread
constructor, then call the start()
method to begin the thread.
4. Thread Lifecycle
A thread in Java goes through several states during its lifetime:
- New: The thread is created but not yet started.
- Runnable: The thread is ready to run and is waiting for CPU time.
- Blocked: The thread is blocked, usually because it is waiting for resources or an I/O operation.
- Waiting: The thread is waiting indefinitely for another thread to perform a particular action (e.g.,
join()
orsleep()
). - Timed Waiting: The thread is waiting for a specified amount of time (e.g.,
sleep()
orjoin()
with a timeout). - Terminated: The thread has completed its execution or was terminated.
Example: Thread Lifecycle
class LifecycleDemo extends Thread {
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) {
LifecycleDemo thread = new LifecycleDemo();
System.out.println("Thread state: " + thread.getState()); // New state
thread.start();
System.out.println("Thread state: " + thread.getState()); // Runnable state
}
}
5. Thread Synchronization
When multiple threads access shared resources concurrently, data inconsistency and errors can occur. Thread synchronization ensures that only one thread can access a resource at a time. Java provides the synchronized
keyword to achieve this.
1. Synchronizing Methods
You can synchronize methods to prevent multiple threads from accessing the method simultaneously.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SyncExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, we use synchronized
to ensure that the increment()
method is thread-safe. This prevents multiple threads from modifying the count
variable simultaneously.
2. Synchronizing Blocks
You can also synchronize specific blocks of code inside methods to control the access to shared resources.
public class SyncBlockExample {
private int count = 0;
public void increment() {
synchronized(this) {
count++;
}
}
}
6. Thread Communication
Threads can communicate with each other using methods like wait()
, notify()
, and notifyAll()
. These methods are used in scenarios where threads need to coordinate and exchange data.
Example: Thread Communication using wait()
and notify()
class Producer implements Runnable {
private final Object lock;
public Producer(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println("Producer: Producing item...");
try {
lock.wait(); // Wait until the consumer consumes
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Producer: Resuming production...");
}
}
}
class Consumer implements Runnable {
private final Object lock;
public Consumer(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println("Consumer: Consuming item...");
lock.notify(); // Notify the producer that consumption is done
}
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
Object lock = new Object();
Thread producer = new Thread(new Producer(lock));
Thread consumer = new Thread(new Consumer(lock));
producer.start();
consumer.start();
}
}
In this example, the producer thread produces an item, and then it waits for the consumer to consume the item before resuming. The consumer notifies the producer once the item is consumed.
7. Exercise
Exercise 1: Create a Multithreaded Program
Create a program that:
- Implements two threads: one for printing odd numbers and the other for printing even numbers.
- Ensure that the odd-number thread prints only odd numbers and the even-number thread prints only even numbers.
Exercise 2: Thread Synchronization
Write a program that simulates a bank account where multiple threads can deposit and withdraw money. Ensure thread safety by synchronizing the methods that modify the account balance.
8. Conclusion
Java threads are a powerful tool for achieving multitasking and parallel execution. By understanding how to create and manage threads, handle thread synchronization, and facilitate thread communication, you can build efficient and responsive applications. Threads help improve the performance of your programs, especially in tasks involving I/O operations, background tasks, or complex calculations.
Through the exercises provided, you will get hands-on experience with creating and managing threads, ensuring thread safety, and implementing thread communication in Java.