Hi! First of all, congratulations: you've reached the topic of 'Multithreading'! This is a serious achievement — you've come a long way. But prepare yourself: this is one of the most difficult topics in the course. And it's not that we're using complex classes or lots of methods here: in fact, we'll use less than twenty. It's more that you'll need to change how you think slightly. Previously, your programs have been executed sequentially. Some lines of code came after others, some methods came after others, and everything was basically clear. First, we calculated something, then displayed result on the console, and then the program ended. To understand multithreading, it's better to think in terms of parallelism. Let's start with something quite simple :) Imagine your family is moving from one house to another. Gathering up all your books will be an important part of the move. You've accumulated lots of books, and you need to put them in boxes. Currently, you're the only one available. Mom is preparing food, brother is packing clothes, and sister went to the store. Alone, you can manage somehow. Sooner or later, you'll complete the task yourself, but it will take a lot of time. However, your sister will return from the store in 20 minutes, and she doesn't have anything else to do. So she can join you. The task hasn't changed: put books into boxes. But it's being performed twice as fast. Why? Because the work is happening in parallel. Two different 'threads' (you and your sister) are performing the same task simultaneously. And if nothing changes, then there will be a huge time difference compared with the situation where you do everything by yourself. If brother finishes his job soon, he can help you and things will go even faster.

Problems solved by multithreading

Multithreading was actually invented to achieve two important objectives:
  1. Do several things at the same time.

    In the example above, different threads (family members) performed several actions in parallel: they washed dishes, went to the store, and packed things.

    We can offer an example more closely related to programming. Suppose you have a program with a user interface. When you click 'Continue' in the program, some calculations should happen and the user should see the following screen. If these actions were performed sequentially, then the program would just hang after the user clicks the 'Continue' button. The user will see the screen with the 'Continue' button screen until the program performs all internal calculations and reaches the part where the user interface is refreshed.

    Well, I guess we'll wait a couple of minutes!

    Or we could rework our program, or, as programmers say, 'parallelize' it. Let's perform our calculations on one thread and draw the user interface on another. Most computers have enough resources to do this. If we take this route, then the program won't freeze up and the user will move smoothly between screens without worrying about what's happening inside. One doesn't interfere with the other :)

  2. Perform calculations more quickly.

    Everything is much simpler here. If our processor has multiple cores, and most processors today do, then several cores can handle our list of tasks in parallel. Obviously, if we need to perform 1000 tasks and each takes one second, one core can finish the list in 1000 seconds, two cores in 500 seconds, three in a little more than 333 seconds, etc.

But as you've already read in this lesson, today's systems are very smart, and on even one computing core are able to achieve parallelism, or rather pseudo-parallelism, where tasks are performed alternately. Let's move generalities to specifics and get to know the most important class in Java multithreading library — java.lang.Thread. Strictly speaking, Java threads are represented by instances of the Thread class. This means that to create and run 10 threads, you need 10 instances of this class. Let's write the simplest example:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
To create and run threads, we need to create a class, make it inherit the java.lang. Thread class, and override its run() method. That last requirement is very important. It is in the run() method that we define the logic for our thread to execute. Now, if we create and run an instance of MyFirstThread, the run() method will display a line with a name: the getName() method displays the thread's 'system' name, which is assigned automatically. But why are we speaking tentatively? Let's create one and find out!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Console output: I'm Thread! My name is Thread-2 I'm Thread! My name is Thread-1 I'm Thread! My name is Thread-0 I'm Thread! My name is Thread-3 I'm Thread! My name is Thread-6 I'm Thread! My name is Thread-7 I'm Thread! My name is Thread-4 I'm Thread! My name is Thread-5 I'm Thread! My name is Thread-9 I'm Thread! My name is Thread-8 Let's create 10 threads (MyFirstThread objects, which inherit Thread) and start them by calling the start() method on each object. After calling the start() method, the logic in the run() method is executed. Note: the thread names are not in order. It's weird that they weren't sequentially: Thread-0, Thread-1, Thread-2, and so on? As it happens, this is an example of a time when 'sequential' thinking doesn't fit. The issue is that we've only provided commands to create and run 10 threads. The thread scheduler, a special operating system mechanism, decides their execution order. Its precise design and decision-making strategy are topics for a deep discussion that we won't dive into right now. The main thing to remember is that the programmer cannot control the execution order of threads. To understand the seriousness of the situation, try running the main() method in the example above a couple more times. Console output on second run: I'm Thread! My name is Thread-0 I'm Thread! My name is Thread-4 I'm Thread! My name is Thread-3 I'm Thread! My name is Thread-2 I'm Thread! My name is Thread-1 I'm Thread! My name is Thread-5 I'm Thread! My name is Thread-6 I'm Thread! My name is Thread-8 I'm Thread! My name is Thread-9 I'm Thread! My name is Thread-7 Console output from the third run: I'm Thread! My name is Thread-0 I'm Thread! My name is Thread-3 I'm Thread! My name is Thread-1 I'm Thread! My name is Thread-2 I'm Thread! My name is Thread-6 I'm Thread! My name is Thread-4 I'm Thread! My name is Thread-9 I'm Thread! My name is Thread-5 I'm Thread! My name is Thread-7 I'm Thread! My name is Thread-8

Problems created by multithreading

In our example with books, you saw that multithreading solves very important tasks and can make our programs faster. Often many times faster. But multithreading is considered to be a difficult topic. Indeed, if used improperly, it creates problems instead of solving them. When I say 'creates problems', I don't mean in some abstract sense. There are two specific problems that multithreading can create: deadlock and race conditions. Deadlock is a situation where multiple threads are waiting for resources held by each other, and none of them can continue to run. We'll talk more about it in subsequent lessons. The following example will suffice for now: Imagine that Thread-1 interacts with some Object-1, and that Thread-2 interacts with Object-2. Furthermore, the program is written so that:
  1. Thread-1 stops interacting with Object-1 and switches to Object-2 as soon as Thread-2 stops interacting with Object-2 and switches to Object-1.
  2. Thread-2 stops interacting with Object-2 and switches to Object-1 as soon as Thread-1 stops interacting with Object-1 and switches to Object-2.
Even without a deep understanding of multithreading, you can easily see that nothing will happen. The threads will never switch places and will wait for each other forever. The error seems obvious, but in reality it is not. You can easily do this in a program. We'll consider examples of code that causes deadlock in subsequent lessons. By the way, Quora has a great real-life example that explains what deadlock is. 'In some states in India, they won't sell you agricultural land unless you're a registered farmer. However, they won't register you as a farmer if you don't own agricultural land'. Great! What can we say?! :) Now let's talk about race conditions. A race condition is a design error in a multithreaded system or application, where the operation of the system or application depends on the order in which parts of the code are executed. Remember, our example where we started threads:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Thread executed: " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Now imagine that the program is responsible for running a robot that cooks food! Thread-0 gets eggs out of the fridge. Thread-1 turns on the stove. Thread-2 gets a pan and puts it on the stove. Thread-3 lights the stove. Thread-4 pours oil into the pan. Thread-5 breaks the eggs and pours them into the pan. Thread-6 throws the eggshells into the trash can. Thread-7 removes the cooked eggs from the burner. Thread-8 puts the cooked eggs on a plate. Thread-9 washes the dishes. Look at the results of our program: Thread executed: Thread-0 Thread executed: Thread-2 Thread executed Thread-1 Thread executed: Thread-4 Thread executed: Thread-9 Thread executed: Thread-5 Thread executed: Thread-8 Thread executed: Thread-7 Thread executed: Thread-3 Is this a comedy routine? :) And all because our program's work depends on the execution order of the threads. Given the slightest violation of the required sequence, our kitchen turns into hell, and an insane robot destroys everything around it. This is also a common problem in multithreaded programming. You'll hear about it more than once. In concluding this lesson, I'd like to recommend a book about multithreading. 'Java Concurrency in Practice' was written in 2006, but has not lost its relevance. It is dedicated to multithreaded Java programming — from the basics to the most common mistakes and antipatterns. If you someday decide to become a multithreading guru, this book is a must-read. See you in the next lessons! :)