From the course: Advanced Java: Hands-on with Streams, Lambda Expressions, Collections, Generics and More

Java concepts for concurrency

- [Instructor] Java provides a lot of tools and APIs for working with concurrency and multithreading. We'll explore some of the Java key concepts that you'll need to understand when working with concurrency. Just know that this is going to be a rather high-level overview. First of all, we have the thread class and the runnable interface. These are used for creating a managing threads. The thread class extends the object class and implements the runnable interface, providing methods for starting, stopping, and managing threads. Let's look at an example in IntelliJ. I have created a class MyThread here that extends the thread, and I've overwritten the run methods saying, Hello from thread. And then, I actually print the thread ID. Then, in thread example, I'm using this MyThread twice to create a new thread, and I'm starting this thread as well. So let's go ahead and run this. And you'll probably see two different threads. Now, you can see it says Hello from thread 15 and 16. Please note that, again, these are different threads from my main method. Next, Java provides synchronization and locks to ensure that shared resources are accessed safely and consistently by multiple threads. Without this, we could not be sure that only one thread is using a resource at a given time. The synchronized keyword can be used to create synchronized methods or blocks that allow only one thread at a time to access a shared resource. Here's a simple example of how to do just that. You can see that the method increment on line six has the synchronized keyword in front of it. That means that if multiple threads call the increment method, only one of them is allowed access at the same time. The others will be queued up, waiting for the increment method to become available again. The java.util.concurrent package provides the executor framework, which simplifies the creation and management of threads. Executors are used to create thread pools that can manage the execution of multiple tasks concurrently while reusing the threads in the pool. Again, this might sound a bit fake, so let's have a look at an example. On line eight, I'm using the executor's class to create a fixed thread pool with five threads in it. This creates an object of type ExecutorService. Then we create a runnable task on line nine, and in this task we'll simply print, Hello from Executor on threads, and then print the thread ID again. Then we call the execute method 10 times providing a task, and that's going to execute a task 10 times utilizing the five threads in the thread pool. After that, we close the ExecutorService with the shutdown method. So I'll run this, and you can see what it does. It has different thread IDs. It's 15, 17, 18, 16, and 19 in there. You can see some of them are used multiple times, and some of them are used only once. So let me run this again. You can see it uses different thread IDs this time. The result can be different every time you run this. That's actually most likely because there's different threads and Java manages the task using the five threads from the thread pool. Working with collections and multiple threads can be problematic. Therefore, Java provides a set of concurrent collections in the java.util.concurrent package that are designed for concurrent access and manipulation by multiple threads. These collections include ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListSet, and quite a few more. Here's an example of ConcurrentHashMap. As you can see, it looks a lot like a normal map, and that's indeed the case. The main difference here is that it can now be safely manipulated and accessed by multiple threads. Another important concept is how to deal with responses that you don't get right away. The future and callable interfaces provide a way to do exactly this. They represent a managed result of a computation that might not be available immediately. Callable is similar to runnable, but it can return the value. And future represents the result of a computation that might not be available yet. Here's an example. We use an ExecutorService with two threads, and we have a task, and the task involves sleeping for a second. Then we go ahead and submit our task to the ExecutorService, and we store the result in a future with generic type integer. And the next part needs to happen in a try catch block. We do future.get to get the result out, but this may throw an interrupted exception or an execution exception, and that's why we need to do it in a try catch block. We then show the result. In the finally block, we shut down the ExecutorService. Okay, let's run this so you can see how this works. And as you can see, it pauses for a bit, and then prints the result. Let me show this to you again. Pauses, prints the result, and that's because the sleep 1000 isn't there. The gets wait until the future has its result in, and then it prints it. There's multiple ways to get the result. We can also specify how long it can take. So, for example, let's specify for 100 milliseconds, and that's not going to be long enough since we specify that it has to wait for a second in there before it's going to return the result with the Thread.sleep. However, this is a problem because now we also need to catch the timeout exception so let's add it to our list of exceptions here, and let's import the timeout exception, and if we'll run it now, it will timeout, and we'll get in the catch. As you can see, the TimeoutException is now thrown, and as you can also see, it still waits for a little bit until it shuts down, and that's because the ExecutorService that's shut down is going to wait for the task to be done first. There are so many more classes, methods and interfaces and concepts that are great for working with concurrency and multithreading in Java, but this is just a high overview and should be enough to get you started to create applications that can handle multiple tasks simultaneously.

Contents