Back to Java
2025-12-138 min read

Java - Thread Control

Learn Java - Thread Control step by step with clear examples and exercises.

Why This Matters

In this full guide on Java's thread control, we will delve into the essential concepts of multithreading, its importance, and how it can help you write efficient and responsive programs. We will also provide practical examples, common mistakes, practice questions, and answers to frequently asked questions to ensure that you fully understand this topic.

The Importance of Multithreading in Java

Multithreading allows multiple tasks or processes to run concurrently within a single program. By creating separate threads, we can improve the responsiveness of our applications by allowing them to perform tasks asynchronously without waiting for each task to complete sequentially. This is particularly useful in scenarios where you need to handle user input, network requests, and other time-consuming operations simultaneously.

Prerequisites

To follow this guide, you should have a good understanding of the following topics:

  1. Basic Java syntax and programming concepts (variables, loops, functions)
  2. Object-oriented programming principles in Java
  3. Exception handling in Java
  4. Understanding of synchronization and concurrency concepts
  5. Familiarity with Java's Thread class and its methods

Core Concept

Creating Threads

In Java, every thread is an instance of the Thread class or a subclass of it. To create a new thread, you can either extend the Thread class and override its run() method or implement the Runnable interface and provide an implementation for its run() method.

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

// Or using Runnable
class MyRunnable implements Runnable {
public void run() {
// Your code here
}
}

After creating a thread, you can start it by invoking the start() method. This will call the run() method and execute the thread's code concurrently with other threads in your program.

Thread Life Cycle

A Java thread goes through several states during its lifetime:

  1. New: The thread is created but not yet started.
  2. Runnable: The thread is ready to run and waiting for the JVM to assign it a CPU.
  3. Running: The thread is currently executing on a CPU.
  4. Blocked: The thread is waiting for a resource (like a lock or I/O operation) and cannot continue execution.
  5. Terminated: The thread has completed its execution and will not run again.

Synchronization

When multiple threads access shared resources, conflicts may occur if the resources are not properly synchronized. To avoid these issues, you can use synchronization mechanisms like synchronized blocks or java.util.concurrent.locks package to ensure that only one thread can access a resource at a time.

Thread Priorities and Daemons

You can set the priority of a thread using its setPriority() method, which takes an integer value between 1 (lowest) and 10 (highest). The JVM may not strictly follow these priorities when scheduling threads, but it can help you control the execution order in some cases.

A daemon thread is a background thread that runs to support other threads in your program. By default, user-created threads are non-daemon, and the JVM will wait for them to complete before exiting. However, if all the non-daemon threads have finished executing and there are still running daemon threads, the JVM will exit immediately.

Thread Pools

A thread pool is a collection of threads managed by an ExecutorService. You can create a thread pool using Executors class, which provides several preconfigured thread pools like newFixedThreadPool(), newCachedThreadPool(), and newScheduledThreadPool(). Using thread pools helps manage the number of threads efficiently and avoid creating too many threads that may consume system resources.

Thread Communication

Thread communication is essential when multiple threads need to share information or coordinate their actions. You can use various methods for thread communication, such as wait(), notify(), and notifyAll() from the Object class, or more advanced mechanisms like CountDownLatch, Semaphore, and CyclicBarrier.

Worked Example

Let's create a simple example with two threads that print numbers from 1 to 10 concurrently:

public class MultiThreadExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> printNumbers(1, 5));
Thread thread2 = new Thread(() -> printNumbers(6, 10));

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

private static void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", Number: " + i);
}
}
}

When you run this code, both threads will execute concurrently, and the output may vary due to the non-deterministic nature of multithreading.

Common Mistakes

  1. Not calling start(): Remember to call start() on a thread after creating it to initiate its execution.
  2. Shared resource conflicts: Make sure to synchronize access to shared resources to avoid conflicts between threads.
  3. Ignoring thread priorities and daemons: Use thread priorities and daemons judiciously to manage your program's behavior.
  4. Creating too many threads: Be aware of the number of threads you create, as excessive thread creation can lead to performance issues and resource exhaustion.
  5. Not using a thread pool: Consider using a thread pool when managing multiple threads to optimize resource usage.
  6. Incorrect use of synchronization mechanisms: Misusing synchronization techniques like synchronized blocks or java.util.concurrent.locks package can lead to deadlocks, livelocks, and other concurrency issues.
  7. Not handling exceptions properly: Make sure to handle exceptions that may occur during thread execution to ensure the program's stability.
  8. Improper use of wait(), notify(), and notifyAll(): Misusing these methods can lead to race conditions, deadlocks, or livelocks if not used correctly.
  9. Not testing for concurrency issues: Always test your multithreaded programs thoroughly to ensure they behave as expected under different thread configurations.

Practice Questions

  1. Write a program that uses two threads to print the numbers 1 to 20, with one thread printing even numbers and the other printing odd numbers.
  2. Implement a simple producer-consumer problem using two threads and a shared buffer. The producer generates random integers and puts them into the buffer, while the consumer removes and prints the integers from the buffer.
  3. Write a program that simulates a concurrent bank account system with multiple accounts and allows users to deposit, withdraw, and check their balances using separate threads for each operation.
  4. Implement a thread-safe implementation of a stack data structure using the java.util.concurrent.locks package.
  5. Write a program that simulates a concurrent game of Tic-Tac-Toe between two players, with each player's moves made by separate threads.
  6. Implement a multithreaded web crawler that downloads multiple web pages simultaneously using the java.net.URL class and handles potential exceptions.
  7. Write a program that reads data from multiple files concurrently using several threads and merges the results into a single output file.
  8. Implement a multithreaded chat server using sockets, where clients can connect and send messages to each other.
  9. Write a program that simulates a concurrent traffic light system with multiple intersections and vehicles moving between them.
  10. Implement a multithreaded application for processing large datasets (e.g., images or text files) by dividing the data into smaller chunks and processing each chunk concurrently using separate threads.

FAQ

  1. Why should I use multithreading in Java?

Multithreading can help improve the responsiveness of your applications by allowing them to perform tasks asynchronously without waiting for each task to complete sequentially. This is particularly useful in scenarios where you need to handle user input, network requests, and other time-consuming operations simultaneously.

  1. How do I create a new thread in Java?

You can either extend the Thread class and override its run() method or implement the Runnable interface and provide an implementation for its run() method. After creating a thread, you can start it by invoking the start() method.

  1. What is the difference between a daemon and a non-daemon thread?

A daemon thread is a background thread that runs to support other threads in your program. By default, user-created threads are non-daemon, and the JVM will wait for them to complete before exiting. However, if all the non-daemon threads have finished executing and there are still running daemon threads, the JVM will exit immediately.

  1. What is a thread pool in Java?

A thread pool is a collection of threads managed by an ExecutorService. It helps manage the number of threads efficiently and avoid creating too many threads that may consume system resources. You can create a thread pool using Executors class, which provides several preconfigured thread pools like newFixedThreadPool(), newCachedThreadPool(), and newScheduledThreadPool().

  1. How do I synchronize access to shared resources in Java?

You can use synchronization mechanisms like synchronized blocks or java.util.concurrent.locks package to ensure that only one thread can access a resource at a time. This helps avoid conflicts between threads when they try to access and modify the same shared resource simultaneously.

  1. What is the difference between wait(), notify(), and notifyAll() in Java?

wait() causes the current thread to wait until it is notified by another thread using notify() or notifyAll(). notify() wakes up one waiting thread, while notifyAll() wakes up all waiting threads.

  1. What are some common concurrency issues in Java and how can they be avoided?

Some common concurrency issues include race conditions, deadlocks, livelocks, and starvation. These issues can be avoided by using proper synchronization mechanisms, designing thread-safe data structures, testing for concurrency issues, and handling exceptions properly.

  1. What is the role of a Producer-Consumer pattern in multithreading?

The Producer-Consumer pattern is a design pattern used to manage shared resources between multiple threads. In this pattern, one or more producer threads generate data (e.g., messages or items) and place them into a buffer, while one or more consumer threads remove the data from the buffer and process it. This pattern helps decouple producers and consumers, allowing them to operate independently while ensuring proper synchronization of shared resources.

  1. What is the difference between a Runnable and Callable in Java?

Runnable is an interface that defines a task that can be executed by a thread. It has a single run() method that returns void. On the other hand, Callable is an interface that defines a task that can return a result (e.g., a value or exception). It has a single call() method that returns a generic T value and may throw exceptions.

  1. What are some best practices for writing multithreaded code in Java?

Some best practices include using proper synchronization mechanisms, designing thread-safe data structures, minimizing shared state between threads, testing for concurrency issues, handling exceptions properly, and using thread pools to manage resources efficiently. Additionally, it's essential to design your multithreaded code with clear responsibilities for each thread and consider potential race conditions, deadlocks, livelocks, and starvation when designing the overall system architecture.

Java - Thread Control | Java | XQA Learn