Get startedGet started for free

Basic multi-threading principles

1. Basic multi-threading principles

Now, let's discuss multi-threading in Java. In today's applications, performance is crucial, and one way to improve it is through parallelism. In this video, we'll explore why single-threaded applications can become bottlenecks and learn how to use multi-threading to improve code performance.

2. Sequential vs parallel execution

Let's start by understanding the difference between sequential and parallel execution. In sequential processing, operations happen one after another. In parallel processing, multiple operations happen simultaneously. Modern CPUs have multiple cores that can perform work concurrently. A thread is the smallest unit of program execution, and multi-threading allows us to distribute work across these cores. We can think of it like this: single-threading is like having a single-lane road where cars must follow one another, whereas multi-threading is like having multiple lanes where cars can travel simultaneously.

3. Using threads

Java's `Thread` class is the fundamental building block for doing this. Each thread can run independently, allowing parallel processing. The typical pattern for thread creation involves creating an anonymous function, also called a lambda expression or a `Runnable`, passing it to a new `Thread` instance, and calling `start()` to begin execution. In this example, we create a `Runnable` that prints the current thread name using `currentThread().getName()`. After creating the `Thread`, we call `start()` to schedule it for execution.

4. Working with multiple threads

For parallel processing, we typically create multiple threads. Here we use a for loop to create four threads, each one including a print statement. After starting all threads, we need to wait until they've all finished their work. That's what the `join()` method does. When we call `join()` on each thread, our program pauses at that point until that thread finishes - allowing us to ensure all processing is complete before moving forward. Without this, our main program might continue before the background work is done.

5. Parallel streams

Java 8 introduced streams, and parallel streams make multi-threading even easier. They automatically handle thread creation and management, letting us focus on the business logic. There are two main ways to create parallel streams: calling `parallelStream()` on a collection or calling `parallel()` to an existing stream.

6. Parallel streams example

Here, we see three code snippets that process a list of numbers. The first uses a traditional for loop to multiply each number by 2 and add it to a result list sequentially. The second uses streams with the `.collect()` method and `Collectors.toList()` to gather results. This processes items sequentially. The third uses a parallel stream to process items simultaneously across multiple threads. This simple change from `stream()` to `parallelStream()` can significantly improve performance for CPU-intensive tasks while the collector manages thread-safe result collection.

7. When to use parallel processing

Not all operations benefit from parallelization. Parallel processing works best for CPU-intensive operations, ideally independent between them (so that multiple threads don't simultaneously edit the same data), large data collections, and when multiple CPU cores are available. Be aware that parallelization has overhead. For small data sets, or simple operations this overhead might outweigh the benefits. Sometimes sequential processing is actually faster!

8. Summary

To summarize, we've explored the `Thread` class for creating parallel execution paths and parallel streams for simplified collection processing. Remember that the performance benefits depend on workload type, data size, and available cores.

9. Let's practice!

Now, let's put our newly learned skills to the test!