Multithreading is not only a hot topic in Java backend development interviews, but also a core cornerstone of advanced tools, frameworks, and distribution. However, the knowledge related to this area covers thread scheduling, thread synchronization, and even more low-level knowledge of hardware primitives and operating systems at some key points. It’s easy to memorize the back of an exam, but it’s easy to give away if the interviewer asks you, let alone if you really want to understand the question and apply it to actual code practice.

Don’t worry! The following series of articles will cover all aspects of this question from the beginning to the end, though not as directly and quickly as some interview guides. But really understanding multi-threaded programming can not only solve the embarrassment in the interview once and for all, but also can open the door to the underlying knowledge, not only to understand an isolated knowledge point, but also a good opportunity to connect the theoretical knowledge of the previous understanding.

No prior knowledge of concurrency concepts is required to read this article, but having a general understanding of the concepts will make it much easier to understand. Interested readers should refer to the first article in this series to understand the basic concepts related to concurrency – what do we mean when we say “concurrency, multithreading”? .

This series will include 10 articles, this is the second article, believe that as long as have the patience to finish see all content will be able to easily play multithreaded programming, not just have room through the interview, but also can master multithreaded programming practice skills and concurrent practice this Java advanced tools and frameworks of common core.

The first five articles contain the following and will be released in the near future:

  1. Concurrency fundamentals – What are we talking about when we say “concurrency, multithreading”?
  2. Introduction to multithreading — this article
  3. Thread pool profiling
  4. Thread synchronization mechanism parsing
  5. Concurrent FAQ

Why multithreading?

Multithreaded program and the general single-threaded program compared to the introduction of synchronization, thread scheduling, memory visibility and a lot of complex problems, greatly improve the difficulty of developers to develop programs, so why now multithreaded in each neighborhood is so popular?

A scenario

When I was in college, there was a rice shop near my dormitory that also provided stir-fried dishes. The boss is very honest, not to order a table in order to cook, if the previous table is not finished after a table do not want to eat a dish. As a result, the store is full of complaints every day, with customers often waiting for half an hour without a dish to fill their stomachs. You ask me why return meeting somebody to eat, suffer this sin, that affirmation is because delicious 😂.

However, if you think about it, it seems that the general restaurant does not seem to have this situation, because most restaurants are mixed, even if the previous table is not finished at least will give a few dishes to fill the stomach. This is the same in programs, different programs can run alternately, so that we will not be able to receive wechat messages when we open development tools on our computers.

This is one example of multithreading: the ability to run multiple programs simultaneously on a single computer by alternating tasks.

Another scenario

Even in small restaurants, a waiter who takes an order for one table does not wait for it to be finished before taking another. It’s common to place an order, give it to the kitchen, and move on to the next table. Here, we can think of the waiters as our computers and the kitchen as a remote server. It’s much more efficient to use our computers by playing music while downloading it.

This scenario can be described as: multi-threading can be used to keep the CPU running while waiting for network requests, disk I/O and other time-consuming operations to complete, in order to achieve efficient utilization of CPU resources.

The last scenario

Then we came to the kitchen, unexpectedly saw a god, can burn 2 stoves a person. If the chef is a multi-core processor, then two cookstoves are two threads, and if only one cookstove is given, his talents are wasted, and it is definitely a loss.

This is the final scenario of multi-threaded applications: splitting a computatively heavy task into two cpus can reduce the execution time, and multi-threading is the carrier of splitting and executing the task, without multi-threading there is no way to put the task on multiple cpus.

What is multithreading?

Multithreading just means lots of threads, HMM, isn’t that easy?

A thread is a unit of execution in an operating system, as is a process, in which all code is executed. Threads are subordinate to processes, and a process can contain multiple threads. Another difference between a process and a thread is that each process has its own independent memory space and cannot access each other directly. However, multiple threads in the same process share the process’s memory space, so they can directly access the same block of memory, the most typical of which is the heap in Java.

Multithreaded programming for the first time

With so many theoretical concepts in mind, it was finally time to actually write code by hand.

Create a thread

Threads in Java are represented by the Thread class. The constructor of the Thread class can pass in an object that implements the Runnable interface. The void run() method in the Runnable object represents the tasks that will be performed in the Thread. For example, if you wanted to create a Runnable task that increments an integer variable, you could write:

Private static int count = 0; // Create Runnable object (anonymous inner class object) Runnable task = newRunnable() {
    public void run() {
        for(int i = 0; i < 1e6; ++i) { count += 1; }}Copy the code

Once we have the pending task represented by the Runnable object, we can create two threads to run it.

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Copy the code

However, only the thread object is created, and the thread is not actually executed. To execute the thread, you need to call the start() method of the thread object.

t1.start();
t2.start();
Copy the code

At this point, the thread can start executing, and the complete code looks like this:

public class SimpleThread {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for(int i = 0; i < 1000000; ++i) { count = count + 1; }}}; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); // wait for t1 and t2 to complete // t1.join(); // t2.join(); System.out.println("count = "+ count); }}Copy the code

The final output is 8251, which should be different when you execute it, but still much less than a million. This seems a bit far from what we would expect, given that each task adds up at least a million times.

This is because we created a thread in the main method and ran it without waiting for the thread to complete. Using T1.join (), we can cause the current thread to wait for the T1 thread to complete before continuing. Let’s test the effect by removing the double slashes in front of the two join method calls.

Thread synchronization

The result on my computer is 1753490. Your result will be different, but it will also fall short of the two million we were hoping for. The specific reason can be found in the following execution sequence diagram.

t1 t2
Get a count value of 0
Get a count value of 0
0+1 gives you 2
Save 2 to count
0+1 gives you 2
Save 2 to count

As you can see, the concurrent running of t1 and T2 threads will cause each other’s results to be overwritten, and the result will be between one and two million, but there will be a large distance from two million. Such a code block in which multiple threads read and modify the same shared data is called a critical section. A critical section allows only one thread to enter at a time. If multiple threads enter at the same time, data race problems will occur. For those of you who are confused by the concepts of critical sections and data competition, please refer to the first article in this series, which introduced the basic concepts of concurrency — what do we mean when we say “concurrency, multithreading”? .

Prior to Java 5, the most common method of thread synchronization was the synchronized keyword, which could be tagged to methods or used as a separate block structure. The synchronized keyword in method declaration form can be used in method definition as follows: public synchronized static void methodName(). Since our accumulation is in the run() method inherited from the Runnable interface, there is no way to change the declaration of the method, so use the synchronized keyword as a block structure:

Runnable task = new Runnable() {
    public void run() {
        for(int i = 0; i < 1000000; ++i) { synchronized (SimpleThread.class) { count += 1; }}}};Copy the code

Synchronized is an object lock. The lock used is related to a specific object. If it is the same object, it is the same lock. If it’s a different object it’s a different lock. Only one thread can hold the lock at a time, which means that other threads trying to acquire the same lock are blocked until the thread holding the lock releases it. Here, the object corresponding to the object lock can be regarded as the name of the lock. The synchronization is not realized by the object itself, but by the object lock corresponding to the object.

In parentheses after the synchronized keyword of the block structure is the object corresponding to the object lock. In the above code, we used the lock corresponding to the class object of the SimpleThread class as a synchronization tool. If synchronized is used in a method declaration, the object to which the instance method (non-static method) corresponds is the object to which the this pointer points, and if static method, the object is the class object of its class.

This time we can see that the output is stable at two million each time now, and we have successfully completed our first full multithreaded program 🎉🎉🎉

Afterword.

However, when we actually write multithreaded code, we typically do not create Thread objects directly, but use Thread pools to manage the execution of tasks. Readers have also seen the term “thread pool” in many places, but if you want to learn more about the use and implementation of thread pools, you should keep an eye on the next article that will be published soon.

So far, we have only touched on the concepts of concurrency and multithreading and simple multithreaded program implementation. We will then move on to deeper and more complex implementations of multithreading, including but not limited to volatile keywords, CAS, AQS, memory visibility, common thread pools, blocking queues, deadlocks, non-deadlocked concurrency, event-driven models, and more. Finally, you can gradually implement a series of concurrent data structures and programs commonly used in various tools, such as AtomicInteger, blocking queues, and event-driven Web servers. I believe you through this series of multithreaded programming adventures will be able to do the topic of multithreading as light, methodical.