Welcome back, future Java master! So far, our programs have mostly been like a single chef working in a kitchen, preparing one dish at a time. But what if you have a huge dinner party and need to prepare many dishes simultaneously? That’s where concurrency comes in!
In this chapter, we’re going to dive into the exciting world of concurrency and multithreading in Java. You’ll learn how to make your programs perform multiple tasks seemingly at the same time, leading to more responsive and efficient applications. This is a crucial skill for building modern, high-performance software. We’ll cover everything from the basic concepts of threads to managing them effectively with Java’s powerful concurrency utilities.
Before we start, make sure you’re comfortable with basic Java syntax, classes, objects, and interfaces from previous chapters. You’re going to build some truly dynamic programs, so let’s get ready to make your code multitask!
Understanding the Juggling Act: Concurrency vs. Parallelism
Before we jump into code, let’s clear up some fundamental ideas. These terms are often used interchangeably, but there’s a subtle yet important difference.
What is Concurrency?
Imagine you’re a single chef (your computer’s CPU) in a kitchen. You might be chopping vegetables for one dish, then quickly stirring a sauce for another, then checking the oven for a third. You’re only doing one thing at any exact moment, but you’re managing multiple tasks over time, giving the illusion that you’re doing them simultaneously.
In programming, concurrency means dealing with many things at once. It’s about designing your program to handle multiple tasks independently, even if they aren’t executing at the exact same instant. It’s about task switching, making progress on multiple fronts.
What is Parallelism?
Now, imagine you have multiple chefs (multiple CPU cores) in the kitchen. Each chef can work on a different dish at the exact same time.
Parallelism means doing many things at once. It requires multiple processing units (like multiple CPU cores) to truly execute different parts of your program simultaneously.
The takeaway: A concurrent program might not be parallel (if it runs on a single core), but a parallel program is always concurrent. Java provides tools to achieve both.
The Building Blocks: Processes and Threads
When your computer runs a program, it creates a process. Think of a process as an entire, self-contained workspace for your program. It has its own memory space, resources, and environment. When you open a web browser, that’s one process. When you open a word processor, that’s another.
Inside each process, there can be one or more threads. A thread is the smallest unit of execution within a process. It’s like a tiny, independent worker within that workspace.
Consider our kitchen analogy again:
- Process: The entire kitchen itself, with all its ingredients, appliances, and space.
- Thread: A single chef working within that kitchen.
A traditional Java program starts with a single thread, often called the “main thread.” But we can create additional threads to perform tasks concurrently.
The Thread Class and Runnable Interface
Java provides two primary ways to create and manage threads:
- Extending the
ThreadClass: You can create a new class that extendsjava.lang.Threadand override itsrun()method. Therun()method contains the code that the new thread will execute. - Implementing the
RunnableInterface: You can create a new class that implements thejava.lang.Runnableinterface and provide an implementation for itsrun()method. Then, you pass an instance of thisRunnableto aThreadconstructor.
Both achieve the same goal (defining a task for a thread), but implementing Runnable is generally preferred. Why? Because Java doesn’t support multiple inheritance, so if your class already extends another class, you can’t extend Thread. Implementing Runnable keeps your class flexible.
Let’s see them in action! We’ll start with the Runnable approach, as it’s the modern best practice.
Step-by-Step Implementation: Building Concurrent Tasks
We’ll start by creating a simple task that prints messages, then launch it using threads.
Step 1: Defining a Task with Runnable
First, let’s define what our thread will actually do. We’ll create a class that implements the Runnable interface.
Create a new Java file named MyTask.java:
// MyTask.java
public class MyTask implements Runnable {
private String taskName;
// Constructor to give our task a name
public MyTask(String name) {
this.taskName = name;
}
// This is the code that the thread will execute
@Override
public void run() {
System.out.println(taskName + " starting...");
try {
// Simulate some work being done
for (int i = 0; i < 5; i++) {
System.out.println(taskName + " working - step " + (i + 1));
Thread.sleep(500); // Pause for 500 milliseconds (half a second)
}
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted!");
Thread.currentThread().interrupt(); // Restore the interrupted status
}
System.out.println(taskName + " finished!");
}
}
Explanation:
public class MyTask implements Runnable: We declareMyTaskto implement theRunnableinterface. This means it promises to provide arun()method.private String taskName;: A simple field to give each task an identifiable name.public MyTask(String name): A constructor to set the task’s name.@Override public void run(): This is the heart of our thread’s work. When aThreadexecutes an instance ofMyTask, it calls thisrun()method.System.out.println(...): We print messages to track the task’s progress.Thread.sleep(500);: This is a very important line!Thread.sleep()makes the current thread pause its execution for a specified number of milliseconds. We use it here to simulate our task doing some actual work and taking time. This helps us observe the concurrent execution.try...catch (InterruptedException e):Thread.sleep()can throw anInterruptedExceptionif another thread tries to interrupt it while it’s sleeping. It’s good practice to catch this.Thread.currentThread().interrupt();is a common pattern to ensure the interrupted status is maintained.
Step 2: Launching Multiple Threads
Now that we have our MyTask defined, let’s create a main program to launch several of these tasks on separate threads.
Create a new Java file named ThreadDemo.java:
// ThreadDemo.java
public class ThreadDemo {
public static void main(String[] args) {
System.out.println("Main thread starting...");
// Create instances of our task
MyTask task1 = new MyTask("Task-A");
MyTask task2 = new MyTask("Task-B");
MyTask task3 = new MyTask("Task-C");
// Create Thread objects, passing our Runnable tasks
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
Thread thread3 = new Thread(task3);
// Start the threads!
// This calls the run() method on each task in a new thread of execution.
thread1.start();
thread2.start();
thread3.start();
System.out.println("Main thread finished launching tasks.");
// The main thread might finish before the other threads, which is normal!
}
}
Explanation:
public static void main(String[] args): This is our main thread, where execution begins.MyTask task1 = new MyTask("Task-A");: We create three instances of ourMyTaskclass, each with a unique name.Thread thread1 = new Thread(task1);: This is where the magic happens! We create aThreadobject, and importantly, we pass ourRunnableinstance (task1) to its constructor. This tells theThreadobject what to run.thread1.start();: This is the crucial method call. Callingstart()on aThreadobject does two things:- It allocates a new thread of execution from the operating system.
- It then calls the
run()method of theRunnableobject (or therun()method overridden in an extendedThreadclass) in that new thread.
What to Observe When You Run ThreadDemo.java:
You’ll notice that the output from “Task-A”, “Task-B”, and “Task-C” will be interleaved. This demonstrates that they are all running concurrently. The “Main thread finished launching tasks.” message will likely appear before all the tasks have completed, because the main thread doesn’t wait for the new threads to finish by default. It just launches them and continues its own execution.
// Example output (will vary slightly each run due to scheduling)
Main thread starting...
Main thread finished launching tasks.
Task-A starting...
Task-B starting...
Task-C starting...
Task-A working - step 1
Task-B working - step 1
Task-C working - step 1
Task-A working - step 2
Task-B working - step 2
Task-C working - step 2
...
Task-B finished!
Task-A finished!
Task-C finished!
Isn’t that cool? Your program is now doing three things (plus the main thread) at once!
Step 3: Dealing with Shared Resources and Race Conditions
Now that we know how to run tasks concurrently, let’s talk about a big problem that arises when multiple threads try to access and modify the same piece of data. This is called a race condition.
Imagine two chefs (threads) trying to update the “sugar remaining” amount on a shared whiteboard (shared resource).
- Chef 1 reads “100g”.
- Chef 2 reads “100g”.
- Chef 1 uses 50g, writes “50g”.
- Chef 2 uses 30g, writes “70g”.
- The final result is “70g”, but it should be “20g” (100 - 50 - 30). This is wrong!
This happens because the operations (read, modify, write) are not atomic (indivisible) and are not synchronized.
Let’s create a scenario to demonstrate this. We’ll have multiple threads incrementing a shared counter.
First, create a SharedCounter.java file:
// SharedCounter.java
public class SharedCounter {
private int count = 0;
public void increment() {
// This operation is NOT atomic! It's actually three steps:
// 1. Read the current value of 'count'
// 2. Add 1 to it
// 3. Write the new value back to 'count'
count++;
}
public int getCount() {
return count;
}
}
Next, create a CounterTask.java that uses this shared counter:
// CounterTask.java
public class CounterTask implements Runnable {
private SharedCounter counter;
private final int incrementsPerThread;
public CounterTask(SharedCounter counter, int increments) {
this.counter = counter;
this.incrementsPerThread = increments;
}
@Override
public void run() {
for (int i = 0; i < incrementsPerThread; i++) {
counter.increment();
}
System.out.println(Thread.currentThread().getName() + " finished its increments.");
}
}
Finally, let’s run a RaceConditionDemo.java to see the problem:
// RaceConditionDemo.java
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
SharedCounter sharedCounter = new SharedCounter();
int numberOfThreads = 5;
int incrementsPerThread = 10000; // Each thread will increment 10,000 times
int expectedCount = numberOfThreads * incrementsPerThread;
System.out.println("Expected count: " + expectedCount);
Thread[] threads = new Thread[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(new CounterTask(sharedCounter, incrementsPerThread), "Worker-" + (i + 1));
threads[i].start();
}
// Wait for all threads to finish
for (int i = 0; i < numberOfThreads; i++) {
threads[i].join(); // 'join()' makes the main thread wait for this thread to die
}
System.out.println("Final count (actual): " + sharedCounter.getCount());
if (sharedCounter.getCount() != expectedCount) {
System.err.println("!!! Race condition detected! Count is incorrect. Expected: " + expectedCount + ", Actual: " + sharedCounter.getCount());
} else {
System.out.println("Count is correct. (Unlikely without synchronization in this scenario)");
}
}
}
Explanation:
SharedCounter: Holds a singlecountvariable. Itsincrement()method directly accessescount++.CounterTask: Each instance gets a reference to the sameSharedCounterobject and callsincrement()many times.RaceConditionDemo: Creates 5 threads, each running aCounterTaskthat increments the shared counter 10,000 times.threads[i].join(): This is important! Thejoin()method makes the current thread (in this case, themainthread) wait until the thread it’s called on (threads[i]) finishes its execution. Withoutjoin(), themainthread would print thefinalCountimmediately, likely before any worker threads even start, resulting in0.- The Problem: When you run
RaceConditionDemo, you will almost certainly see that “Race condition detected! Count is incorrect.” The final count will be less than the expected 50,000. This is because multiple threads are reading, incrementing, and writing thecountvariable without proper coordination, leading to lost updates.
Step 4: Fixing Race Conditions with synchronized
Java provides mechanisms to ensure that only one thread can access a critical section of code (where shared resources are modified) at a time. The most fundamental mechanism is the synchronized keyword.
When a method or a block of code is synchronized, Java ensures that only one thread can execute that code at any given moment for a particular object. It uses an intrinsic lock (monitor) associated with every Java object.
Let’s modify our SharedCounter class to make the increment() method synchronized.
// SharedCounter.java (Modified)
public class SharedCounter {
private int count = 0;
// The 'synchronized' keyword ensures that only one thread can execute
// this method at a time for a given SharedCounter object instance.
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Now, re-run RaceConditionDemo.java with this modified SharedCounter.
What to Observe:
This time, the output should consistently show:
Final count (actual): 50000
Count is correct.
Success! By adding synchronized to the increment() method, we’ve ensured that when one thread is executing increment(), no other thread can enter that same increment() method on the same SharedCounter object until the first thread exits it. This prevents the race condition.
Important Note on synchronized:
- Synchronized Methods: When a non-static method is
synchronized, the lock is acquired on thethisobject (the instance of the class). - Synchronized Static Methods: When a static method is
synchronized, the lock is acquired on theClassobject itself. - Synchronized Blocks: You can also synchronize a specific block of code using
synchronized (objectReference) { ... }. This allows for finer-grained control, synchronizing only the truly critical section and holding the lock on a specific object. This is often preferred over synchronizing an entire method if only a small part of the method needs protection.
For our SharedCounter, synchronized on the method is perfectly fine and clear.
Step 5: Modern Concurrency with java.util.concurrent - ExecutorService
While directly creating and managing Thread objects works, it can become cumbersome in larger applications. What if you need to limit the number of threads, reuse them, or manage their lifecycle more gracefully?
Enter java.util.concurrent, introduced in Java 5 and significantly enhanced over the years, which provides a powerful framework for concurrent programming. The core component for managing threads is the ExecutorService.
An ExecutorService manages a pool of threads. Instead of creating a new Thread for each task, you submit your Runnable (or Callable) tasks to the ExecutorService, and it takes care of running them using its internal thread pool. This provides:
- Resource Management: Limits the number of threads, preventing your application from creating too many threads and exhausting system resources.
- Performance: Reuses existing threads, avoiding the overhead of creating new threads for each task.
- Task Management: Provides mechanisms to submit tasks, get results, and shut down the pool.
Let’s refactor our MyTask example to use ExecutorService.
// MyTask.java (Same as before, it's a Runnable)
public class MyTask implements Runnable {
private String taskName;
public MyTask(String name) { this.taskName = name; }
@Override
public void run() {
System.out.println(taskName + " starting...");
try {
for (int i = 0; i < 3; i++) { // Reduced loops for quicker demo
System.out.println(taskName + " working - step " + (i + 1));
Thread.sleep(300);
}
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted!");
Thread.currentThread().interrupt();
}
System.out.println(taskName + " finished!");
}
}
Now, create ExecutorServiceDemo.java:
// ExecutorServiceDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo {
public static void main(String[] args) {
System.out.println("Main thread starting...");
// 1. Create an ExecutorService with a fixed-size thread pool
// This pool will have 2 threads, meaning only 2 tasks can run concurrently
// from this pool at any given time.
ExecutorService executor = Executors.newFixedThreadPool(2);
// 2. Submit tasks to the executor
executor.submit(new MyTask("Pool-Task-1"));
executor.submit(new MyTask("Pool-Task-2"));
executor.submit(new MyTask("Pool-Task-3")); // This task will wait for a thread to become free
executor.submit(new MyTask("Pool-Task-4"));
// 3. Shut down the executor service
// No new tasks can be submitted, but previously submitted tasks will complete.
executor.shutdown();
// 4. (Optional) Wait for all tasks to complete
try {
// This line makes the main thread wait for all tasks submitted to the executor
// to complete, or until 60 seconds pass, whichever comes first.
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Executor did not terminate in the allotted time.");
executor.shutdownNow(); // Forcefully shut down if it takes too long
}
} catch (InterruptedException e) {
System.err.println("Main thread interrupted while waiting for executor termination.");
executor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("Main thread finished all tasks and shut down executor.");
}
}
Explanation:
import java.util.concurrent.ExecutorService;,import java.util.concurrent.Executors;,import java.util.concurrent.TimeUnit;: We import the necessary classes from thejava.util.concurrentpackage.ExecutorService executor = Executors.newFixedThreadPool(2);: This line creates anExecutorServicethat manages a pool of exactly 2 threads. If you submit more than 2 tasks, the extra tasks will wait in a queue until a thread becomes available.executor.submit(new MyTask("Pool-Task-1"));: Instead ofnew Thread(...).start(), we now useexecutor.submit(). TheExecutorServicehandles assigning theRunnabletask to an available thread from its pool.executor.shutdown();: It’s crucial to callshutdown()when you’re done submitting tasks. This tells theExecutorServiceto stop accepting new tasks and to gracefully shut down once all currently running tasks (and tasks in the queue) are completed.executor.awaitTermination(60, TimeUnit.SECONDS): This is similar tothread.join(). It makes themainthread wait for theExecutorServiceto finish all its tasks. It’s good practice to provide a timeout to prevent the main thread from waiting indefinitely. If the timeout expires, it returnsfalse.executor.shutdownNow(): This is a more aggressive shutdown that attempts to stop all executing tasks immediately and prevents queued tasks from ever starting. Use with caution.
What to Observe When You Run ExecutorServiceDemo.java:
You’ll see that “Pool-Task-1” and “Pool-Task-2” will start executing concurrently. “Pool-Task-3” and “Pool-Task-4” will only start after one of the first two tasks finishes, because our pool size is 2. The output will clearly show tasks taking turns using the limited number of threads. The main thread will wait until all tasks are truly finished before printing its final message.
Step 6: Getting Results from Threads with Callable and Future
What if your concurrent task needs to return a value? Runnable’s run() method returns void. For tasks that produce a result, Java provides the Callable interface.
Callable<V>: Similar toRunnable, but itscall()method returns a value of typeVand can throw checked exceptions.Future<V>: When you submit aCallableto anExecutorService, it returns aFutureobject. TheFuturerepresents the result of an asynchronous computation. You can use it to check if the task is complete, cancel it, or retrieve its result using theget()method. Theget()method blocks until the result is available.
Let’s create a task that calculates a sum and returns it.
First, create SummingTask.java:
// SummingTask.java
import java.util.concurrent.Callable;
public class SummingTask implements Callable<Integer> { // Callable now specifies the return type: Integer
private int start;
private int end;
private String name;
public SummingTask(String name, int start, int end) {
this.name = name;
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception { // call() returns Integer and can throw Exception
System.out.println(name + " starting to sum from " + start + " to " + end);
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
// Simulate work and allow other tasks to run
Thread.sleep(10);
}
System.out.println(name + " finished. Sum: " + sum);
return sum; // Return the calculated sum
}
}
Next, create CallableFutureDemo.java:
// CallableFutureDemo.java
import java.util.concurrent.*; // Import everything from concurrent for convenience
public class CallableFutureDemo {
public static void main(String[] args) {
System.out.println("Main thread starting...");
ExecutorService executor = Executors.newFixedThreadPool(3); // A pool of 3 threads
// Submit Callable tasks and get Future objects
Future<Integer> future1 = executor.submit(new SummingTask("Summer-1", 1, 10));
Future<Integer> future2 = executor.submit(new SummingTask("Summer-2", 11, 20));
Future<Integer> future3 = executor.submit(new SummingTask("Summer-3", 21, 30));
// Let's try to submit one more, it will wait for a thread to free up
Future<Integer> future4 = executor.submit(new SummingTask("Summer-4", 31, 40));
// Retrieve results from Future objects
try {
System.out.println("Main thread trying to get results...");
// .get() is a blocking call! The main thread will pause here until the result is available.
Integer result1 = future1.get();
System.out.println("Result from Summer-1: " + result1); // Expected: 55
Integer result2 = future2.get();
System.out.println("Result from Summer-2: " + result2); // Expected: 155
Integer result3 = future3.get();
System.out.println("Result from Summer-3: " + result3); // Expected: 255
Integer result4 = future4.get();
System.out.println("Result from Summer-4: " + result4); // Expected: 355
int totalSum = result1 + result2 + result3 + result4;
System.out.println("Total sum from all tasks: " + totalSum); // Expected: 820
} catch (InterruptedException | ExecutionException e) {
System.err.println("Error retrieving task result: " + e.getMessage());
e.printStackTrace();
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
System.out.println("Main thread finished all operations.");
}
}
Explanation:
public class SummingTask implements Callable<Integer>:SummingTasknow implementsCallableand specifiesIntegeras its return type.@Override public Integer call() throws Exception: The method is nowcall()and returns anInteger. It can also throwException(or a more specific checked exception).return sum;: Thecall()method explicitly returns the calculated sum.Future<Integer> future1 = executor.submit(new SummingTask("Summer-1", 1, 10));: When you submit aCallableto anExecutorService, it returns aFutureobject. ThisFutureis a placeholder for the result that will eventually be computed.Integer result1 = future1.get();: Theget()method onFutureis used to retrieve the result. Crucially,get()is a blocking call. Themainthread will pause at this line untilSummer-1has completed itscall()method and returned its result. If the task threw an exception,get()would throw anExecutionException.
What to Observe When You Run CallableFutureDemo.java:
You’ll see the SummingTasks starting concurrently. The main thread will print “Main thread trying to get results…” and then pause as it calls future1.get(). Once Summer-1 finishes, its result will be printed, and main will proceed to future2.get(), and so on. Even though tasks run concurrently, get() forces sequential retrieval of their results in the main thread.
This pattern is extremely powerful for offloading heavy computations to background threads and then collecting their results when needed.
Mini-Challenge: Concurrent File Processing Simulation
You’ve learned how to define tasks, launch them with threads, handle race conditions, and use ExecutorService with Callable and Future. Now, let’s put it all together in a small simulation!
Challenge: Create a program that simulates processing multiple “data files” concurrently. Each “file processing” task should:
- Be defined using the
Callableinterface, returning aStringindicating its success or failure. - Take a
Stringfilename and anintprocessingTime (in milliseconds) as constructor arguments. - Inside its
call()method, print messages like “Processing file [filename]…” and “Finished processing [filename] in [time]ms.” - Simulate processing time using
Thread.sleep(). - Randomly (e.g., 1 in 10 chance) throw an
Exceptionto simulate a processing error, returning an error message string. - Use an
ExecutorServicewith a fixed thread pool (e.g., 3 threads) to run 5-7 such processing tasks. - Collect all the
Futureresults and print the status for each file (e.g., “File report.csv: SUCCESS” or “File error.log: FAILED - [error message]”).
Hint:
- You can use
new Random().nextInt(10) == 0to simulate a 10% chance of an error. - Remember to handle
InterruptedExceptionandExecutionExceptionwhen callingfuture.get(). - Ensure the
ExecutorServiceis properly shut down.
What to observe/learn:
- How
ExecutorServicemanages a limited number of concurrent tasks. - How
Callableallows tasks to return specific results. - How
Futureallows you to collect those results (and handle potential errors) later. - The benefit of concurrency in handling multiple independent operations.
Common Pitfalls & Troubleshooting in Concurrency
Concurrency is powerful, but it’s also notoriously tricky. Even experienced developers can fall into these traps.
Forgetting Synchronization (Race Conditions): This is the most common mistake. Anytime multiple threads read and write to shared mutable data, you must have a synchronization mechanism (like
synchronized,Locks, or atomic variables) in place. If you forget, you’ll get inconsistent, incorrect, and hard-to-debug results, like ourSharedCounterexample.- Troubleshooting: If your program produces incorrect results intermittently, especially when dealing with shared state, a race condition is highly likely. Carefully review all access points to shared variables.
Deadlock: This occurs when two or more threads are blocked indefinitely, each waiting for the other to release a resource.
- Example: Thread A holds Lock X and wants Lock Y. Thread B holds Lock Y and wants Lock X. Both wait forever.
- Troubleshooting: Deadlocks are tricky because they often appear under specific timing conditions. Use tools like
jstack(a command-line utility in the JDK) to dump thread stack traces. If threads are in aBLOCKEDstate, you can often see which lock they are waiting for and which thread owns it. IDEs like IntelliJ or Eclipse also have built-in thread dumping features.
Livelock and Starvation:
- Livelock: Threads are not blocked, but they are constantly reacting to each other’s actions in a way that prevents any actual progress. Imagine two people trying to pass each other in a narrow hallway, both repeatedly stepping aside in the same direction.
- Starvation: A thread consistently loses the race for acquiring a shared resource or CPU time, and thus never makes progress, even though the resource eventually becomes available. This can happen with unfair locking mechanisms or poorly designed thread priorities.
- Troubleshooting: These are harder to detect than deadlocks. Often, profiling tools or careful logging of thread activities are needed to identify why a thread isn’t making progress.
Improper Shutdown of
ExecutorService: Forgetting to callexecutor.shutdown()can lead to your application hanging, as the non-daemon threads within the thread pool might prevent the JVM from exiting.- Troubleshooting: Always ensure
shutdown()is called, preferably in afinallyblock or application shutdown hook. UseawaitTermination()to allow for graceful shutdown.
- Troubleshooting: Always ensure
Not Handling
InterruptedException: When a thread is blocked (e.g., bysleep(),wait(),join(), orget()onFuture), another thread can call itsinterrupt()method. This doesn’t immediately stop the thread; instead, it causes anInterruptedException. It’s crucial to catch this exception and decide how to respond (e.g., clean up and exit, or set the interrupted status againThread.currentThread().interrupt();).- Troubleshooting: If your threads are unresponsive to external signals, check if
InterruptedExceptionis being caught and handled correctly.
- Troubleshooting: If your threads are unresponsive to external signals, check if
Summary: Mastering the Art of Multitasking
Phew! We’ve covered a lot in this chapter. Concurrency is a vast and fascinating topic, and you’ve taken huge strides in understanding its fundamentals.
Here are the key takeaways from Chapter 11:
- Concurrency vs. Parallelism: Concurrency is about managing many tasks, parallelism is about doing many tasks simultaneously.
- Threads are Workers: Threads are the smallest units of execution within a process, allowing your program to multitask.
Runnableis Preferred: Define your thread’s work by implementing theRunnableinterface and overriding itsrun()method.Thread.start()Launches: Callstart()on aThreadobject to begin its execution in a new thread. Never callrun()directly!- Race Conditions are Dangerous: When multiple threads access and modify shared mutable data without proper synchronization, you get unpredictable and incorrect results.
synchronizedfor Safety: Use thesynchronizedkeyword (on methods or blocks) to protect critical sections of code, ensuring only one thread can execute them at a time for a given object.ExecutorServiceis Your Manager: For robust thread management, useExecutorServicefromjava.util.concurrent. It manages thread pools, reuses threads, and simplifies task submission.CallableandFuturefor Results: UseCallablefor tasks that need to return a value, andFutureto retrieve that value (and handle potential exceptions) from theExecutorService.- Graceful Shutdown: Always remember to call
executor.shutdown()and optionallyawaitTermination()to properly shut down your thread pools. - Beware of Pitfalls: Race conditions, deadlocks, and improper exception handling are common challenges in concurrent programming.
You’ve now got the foundational knowledge to make your Java applications more responsive and efficient by leveraging the power of multiple threads. This is a critical skill for building modern, high-performance systems.
What’s Next?
In the next chapter, we’ll continue our journey into advanced Java topics. We’ll explore more sophisticated concurrency utilities, delve into design patterns that are crucial for building robust applications, and discuss how to keep your code clean and maintainable. Get ready to build even more powerful and elegant Java solutions!