Thread basics of Java multi-threading (very detailed~)


Preface

Multi-threading is a difficult part to understand in Java, especially when asked frequently during interviews. However, we have never systematically sorted out the knowledge points involved in multi-threading. So without further ado, in this article I want to combine it with specific code cases. Let’s sort out the basic content of multi-threading. Finally, I want to say that it is really not possible to memorize these knowledge points by rote. You must understand and understand! ! !


1. The difference between processes and threads

Insert image description here
The picture above shows two applications, but they cannot be called processes. When we double-click to start the program, the instructions are given to the CPU for execution, and the data is loaded from the disk to the memory. At this time, the running program is a process, and the process is divided into For singleton processes and multi-case processes, for example, Notepad can be opened multiple times at the same time and it is a multi-case, while Kugou Music can only open one and it is a singleton. The above is the definition of a process, so what are processes and threads? What about relationships?

  • A process is a running program instance. The process contains threads, and each thread performs different tasks.
  • Different processes use different memory spaces, and all threads in the current process can share the memory space.
  • Threads are more lightweight, and thread context switching costs are generally lower than process context switching (context switching refers to switching from one thread to another)

2. The difference between parallelism and concurrency

In the case of a single-core CPU, threads are actually executed serially. We know that within the operating system, the CPU is divided into time slices for threads to execute. The execution time of each thread is about 15ms, which is very difficult for humans. What cannot be perceived can actually be understood this way, that is, macroscopic parallelism and microscopic serialization. Therefore, concurrency can be understood as the phenomenon of multiple threads taking turns using the CPU. So what is the difference between parallelism and concurrency? To give a simple example, a housewife has to do many things at the same time. She has to feed the baby, clean, and cook. She takes turns doing these things by herself, which is concurrency. To give another example, for example, the housewife hires a nanny, and they do these things together. At this time, there is both parallelism and concurrency (competition will occur at this time, there is only one pot, and while one person is using the pot, the other person can only wait) , if this housewife hires three nannies, one specializes in cooking, one specializes in cleaning, and one specializes in breastfeeding, and they do not interfere with each other, then it is parallel. Summarized as follows:

  • Concurrency is the ability to handle multiple things at the same time, with multiple threads taking turns using one or more CPUs
  • Parallelism is the ability to do multiple things at the same time. A 4-core CPU executes 4 threads at the same time.

3. How to create threads (key points)

  • Create threads by inheriting the Thread class
  • Create threads by implementing the Runnable interface
  • Create threads by implementing the Callable interface
  • Create threads through thread pools (commonly used in work)

Next, let’s learn about these ways to create threads through code.

1. Inherit the Thread class

public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        System.out.println("Mythread run...");
    }

    public static void main(String[] args) {
    
    
        MyThread thread01 = new MyThread();
        thread01.start();
    }
}

2. Implement the Runnable interface

public class MyRunnable implements Runnable{
    
    

    @Override
    public void run() {
    
    
        System.out.println("MyRunnable is running...");
    }

    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

What we note here is that we need to first create a class object that implements the Runnable interface, and then pass the object into the Thread constructor before it can run

3. Implement the Callable interface

public class MyCallable implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        System.out.println(Thread.currentThread().getName());
        return "OK";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("线程运行后的返回结果:" + futureTask.get());
    }

}

Here we can combine FutureTask to obtain the result of thread asynchronous execution. This is also a difference between Callable and Runnable to create threads, which will be mentioned later.

4. Use thread pool to create threads

public class MyExecutors implements Runnable{
    
    
    @Override
    public void run() {
    
    
        try{
    
    
            System.out.println("MyExecutors is running...");
        }catch (Exception e){
    
    }
    }

    public static void main(String[] args) {
    
    
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors());
        // 关闭线程池
        threadPool.shutdown();
    }
}

4. What is the difference between Runnable and Callable?

1. The run method of the Runnable interface has no return value

Insert image description here
We can see that the method return value of the Runnable interface is void type

2. The call method of the Callable interface has a return value and is a generic type.

Insert image description here

3.Callable interface can be used in conjunction with Future and FutureTask

The Callable interface can be used in conjunction with Future and FutureTask to obtain asynchronous execution results. The thread execution results can be obtained through the future.get() method.
Insert image description here

4. The call() method of the Callable interface allows throwing exceptions

Insert image description here

5. The run() method of the Runnable interface cannot be thrown upward.

Insert image description here
An error is reported during the compilation phase. It can be seen that the run() method cannot throw an exception, but it can be digested internally using try-catch.
Insert image description here

5. Several states of threads

We can open the Thread class, which defines an internal enumeration class that describes several states of the thread from creation to death.
Insert image description here
From the above definition, we can see that it has a total of 6 states, namely:

NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED

In fact, we can simplify it and understand it better. From new, a thread is newly created. When we call the start() method of the thread, it is in the runnable state. After the thread is finished running, it is the terminated state, and the runnable state is divided into Runnable, blocking, waiting, and time waiting states, aren't these states caused by calling the sleep() method or wait() method or not acquiring the lock when adding a synchronized lock? We can see it more clearly by looking at the picture below. Intuitively understand the principles behind these states:
Insert image description here
So to summarize, the state switching process between threads is as follows:

  • Creating a thread object is in a new state
  • The start() method is called and becomes executable.
  • The thread has obtained the execution right of the CPU, and the execution is terminated.
  • During the executable state, if the execution right of the CPU is not obtained, it may switch to other states (blocking, waiting, timed waiting)
  • If the lock is not acquired, it will enter the blocking state at this time. After acquiring the lock, it will switch to the executable state.
  • If a thread calls the wait() method and enters the waiting state, other threads can switch to the executable state after calling notify() to wake up.
  • If the thread calls the sleep(50) method, it enters the timing waiting state and can switch to the executable state after the time is up.

6. The difference between notify() and notifyAll()

This is actually easier to understand. You will know the meaning when you see the name.

  • notify: Randomly wake up a thread
  • notifyAll: wake up all wait threads
    . Put a case, paste the code only once, call the notify() and notifyAll() methods of the object respectively, and you can know by looking at the execution results.
public class WaitNotify {
    
    
    static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException{
    
    
        // create t1 thread
        Thread t1 = new Thread(()->{
    
    
            synchronized (obj){
    
    
                try {
    
    
                    System.out.println("t1 is start...");
                    obj.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println("t1被唤醒了...");
            }
        }, "t1");
        // create t2 thread
        Thread t2 = new Thread(()->{
    
    
            synchronized (obj){
    
    
                try {
    
    
                    System.out.println("t2 is start...");
                    obj.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println("t2被唤醒了...");
            }
        }, "t2");
        // 主线程
        t1.start();
        t2.start();
        // 主线程睡眠
        Thread.sleep(3000);
        // 主线程获取对象锁
        synchronized (obj){
    
    
            // 随机唤醒一个线程
           obj.notify();
            // 唤醒所有线程
            //obj.notifyAll();
        }

    }
}

Let's analyze the above code. We created two threads in the main method, namely t1 and t2. Both threads use the same synchronized object lock. The main thread also uses the same object as the above two threads. Lock, when calling the start() method of two threads, no matter which thread executes first, these two sentences will be output (t1 is start..., t2 is start...) because after a thread acquires the object lock, it immediately calls wait () method enters the waiting state, then another thread can obtain the object lock. After the main thread sleeps for 3 seconds, the main thread obtains the object lock and calls the object's notify() method to randomly wake up one of the threads. Let's look at the execution results:
Insert image description here
We found that the program did not exit, because only the t1 thread was awakened at this time, and the t2 thread was still running.

When we call the notifyAll() method of the object, all threads in the wait state will be awakened to see the program running results:
Insert image description here
Insert image description here
we can see that the program has stopped, indicating that both threads have completed execution and there are no threads in the waiting state.

7. Create three new threads t1, t2, and t3. How to ensure that they are executed in order?

  • You can use the join() method in the thread to solve the problem
  • t.join() blocks the thread calling this method and enters the timing waiting state. This thread will continue to execute until thread t completes execution.

code show as below:

public class JoinTest {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(()->{
    
    
            try{
    
    
                System.out.println("t1 is start...");
            }catch (Exception e){
    
    }
        });

        Thread t2 = new Thread(()->{
    
    
           try{
    
    
               t1.join();
           }catch (Exception e){
    
    }
            System.out.println("t2 is start...");
        });

        Thread t3 = new Thread(()->{
    
    
            try{
    
    
                t2.join();
            }catch (Exception e){
    
    }
            System.out.println("t3 is start...");
        });
        t3.start();
        t2.start();
        t1.start();
    }
}

In the above code, we created three threads, namely t1, t2, and t3. The join() method of t1 was called in t2, and the join() method of t2 was called in t3. Therefore, no matter which thread's start() ) written in front, the execution order of threads must be t1>t2>t3.
The program running results are as follows:
Insert image description here

8. What is the difference between the thread’s run() method and start() method?

Have you often encountered this question in interviews? It is actually very basic, but the answers we usually give are not so satisfying. Here we will briefly summarize it. In addition, we can see the difference between the two by simply running the program:

  • start(): used to start a thread and call the logic code defined in the run() method through the thread. The start() method can only be called once
  • run(): encapsulates the code to be executed by the thread and can be called multiple times
public class StartAndRunTest {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread("t1"){
    
    
            @Override
            public void run() {
    
    
                System.out.println("t1 is running...");
            }
        };
        t1.start();
        t1.run();
        t1.run();
//        t1.start();
    }
}

The effect is the same when we call the start() method and the run() method. The code in the run method body will be executed. Look at the running results:
Insert image description here
when we execute the start() method multiple times, an exception will be thrown.
Insert image description here
Insert image description here

9. The difference between wait() method and sleep() method in Java

This is also a question that is often asked. Can we answer it all? They have similarities and differences. Let’s summarize it first:

1. Common points

  • The effect of wait(), wait(long) and sleep(long) is to make the current thread temporarily give up the right to use the CPU and enter the blocking state.

2. Differences

  • The method ownership is different. sleep(long) is a static method of Thread, while wait() and wait(long) are member methods of Object(). Each object has
  • The timing of waking up is different. The threads executing sleep(long) and wait(long) will wake up after waiting for the corresponding milliseconds. wait(long) and wait() can also be awakened by notify or notify(). If wait() If you don't wake up, they will keep waiting. They can all be interrupted and woken up.
  • The lock characteristics are different (key point): The call to the wait() method must first acquire the lock of the wait() object, but sleep has no such restriction. After the wait() method is executed, the object lock will be released, allowing other threads to obtain the object lock (I Give up the CPU, but you can still use it) and if sleep is executed in a synchronized code block, it will not release the object lock (I give up the CPU, but you can't use it)

Text descriptions are difficult to understand. We can analyze specific cases to understand the above concepts:

public class WaitSleepCase {
    
    
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        illegalWait();
    }

    /**
     * @throws InterruptedException
     */
    private static void illegalWait() throws InterruptedException {
    
    
        synchronized (LOCK){
    
    
            System.out.println("LOCK is waiting");
            LOCK.wait();
        }
    }
}

Above we defined an illegalWait method, used a synchronized code block in the method body, and called the object's wait() method in the code block to see the program running results: the program ran normally without any problems, and the process did not stop because the thread was in wait
Insert image description here
. status and has not been awakened.
Insert image description here
Insert image description here
If we don't call the wait() method in synchronized, an exception will be reported!
Next we continue to look at another case:

public class WaitSleepCase {
    
    
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        waiting();
    }

    private static void waiting() throws InterruptedException {
    
    
        Thread t1 = new Thread(()->{
    
    
            synchronized (LOCK){
    
    
                try {
    
    
                    System.out.println("t1 is waiting...");
                    LOCK.wait(5000L);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println("t1 is running and end...");
            }
        }, "t1");

        t1.start();
        Thread.sleep(100);
        synchronized (LOCK){
    
    
            System.out.println("主线程获取了锁...");
        }
    }
  }

In the above code, we defined the waiting method. In the method, we created thread t1, used synchronized code block in thread t1, and called the wait(5000L) method of the object. After thread t1 started, it obtained the object lock, and then it will Sleeping for 5s, the main thread sleeps for 0.1s. Then the main thread acquires the object lock and prints the statement. Finally, after 5s, thread t1 acquires the object lock and finally exits. The execution results are as follows: The
last
Insert image description here
case

public class WaitSleepCase {
    
    
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        sleeping();
    }

    private static void sleeping() throws InterruptedException {
    
    
        Thread t2 = new Thread(()->{
    
    
            synchronized (LOCK){
    
    
                try {
    
    
                    System.out.println("t2 is start...");
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println("t2 is running and end...");
            }
        },"t2");
        t2.start();
        Thread.sleep(300);
        synchronized (LOCK){
    
    
            System.out.println("主线程获取了对象锁...");
        }
    }
  }

The above code defines a sleeping method and creates the t2 thread. The synchronized code block is also used in the thread. The static method sleep(5000L) of Thread is used in the code block. After the t2 thread is started, the t2 thread acquires the object lock and then sleeps for 5 seconds. , the difference between it and wait(5000L) is that during the 5s it sleeps, no other thread can get the object lock, so the main thread cannot get the object lock, unless thread t2 releases the object lock after 5s. The main thread can acquire the object lock again.
The running results are as follows:
Insert image description here

10. How to stop a running thread

  • Use the exit flag to make the thread exit normally, that is, the thread terminates after the run method is completed.
public class MyInterrupt1 extends Thread{
    
    
    volatile boolean flag = false;

    @Override
    public void run() {
    
    
        while (!flag){
    
    
            System.out.println("MyThread...running...");
            try {
    
    
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 启动线程
        MyInterrupt1 myInterrupt1 = new MyInterrupt1();
        myInterrupt1.start();
        // 线程睡眠6000ms
        Thread.sleep(6000L);
        // 改变boolean类型值为true
        myInterrupt1.flag = true;
    }
}

We defined a Boolean type flag and modified it with the volatile keyword, indicating that the modification of the shared variable is visible between threads. We created a thread by inheriting the Thread class, and used a while in the thread's run method. Loop, the condition is !flag, because the initial value is false, if it is not modified, this is an infinite loop. After sleeping for 6 seconds in the main thread, modify the boolean type flag to true, then the thread run() method will The while loop will exit and the thread will end running. The running results are as follows:
Insert image description here

  • Use the stop method to forcefully terminate (not recommended, the method is obsolete)
public class MyInterrupt2 extends Thread{
    
    
    volatile boolean flag = false;

    @Override
    public void run() {
    
    
        while (!flag){
    
    
            System.out.println("MyThread...running...");
            try {
    
    
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        // 启动线程
        MyInterrupt2 myInterrupt1 = new MyInterrupt2();
        myInterrupt1.start();
        // 线程睡眠6000ms
        Thread.sleep(6000L);
        // 调用stop方法
        myInterrupt1.stop();
    }
}

This code is basically the same as the above code, except that there is an additional call to stop() method. Let's take a look at the running results:
Insert image description here

  • Use the interrupt method to interrupt a thread. The thread that interrupts the blocked thread (sleep, wait, join) will throw an InterruptedException exception and interrupt the normal thread. You can mark whether to exit the thread according to the interrupt status.

Next we look at another way to interrupt a thread, by calling the interrupt method.

public class MyInterrupt3 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 打断sleep状态线程
        Thread t1 = new Thread(()->{
    
    
            System.out.println("t1...running...");
            try {
    
    
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
        }, "t1");
        // 启动t1线程
        t1.start();
        // 主线程睡眠
        Thread.sleep(500L);
        // 调用interrupt方法中断线程
        t1.interrupt();
        // 中断后的状态
        System.out.println(t1.isInterrupted());
}
}

The running results are as follows:
Insert image description here

public class MyInterrupt3 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //打断正常状态线程
        Thread t2 = new Thread(()->{
    
    
            while (true){
    
    
                System.out.println("t2...running...");
                Thread thread = Thread.currentThread();
                boolean interrupted = thread.isInterrupted();
                if (interrupted){
    
    
                    System.out.println("打断状态:" + interrupted);
                    break;
                }
            }
        }, "t2");

        // 启动t2线程
        t2.start();
        // 主线程睡眠
        Thread.sleep(500L);
        // 打断t2线程
        t2.interrupt();
}
}

The running results are as follows:
Insert image description here

11. A basic thread written test question

I came across a written test question not long ago. If you guys have any ideas, you can type the answer in the comment area. The code is as follows:

public class ThreadExample {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        StringBuilder str = new StringBuilder();
        Thread t1 = new Thread(()->{
    
    
           synchronized (str){
    
    
               str.append("A");
               try{
    
    
                   str.wait(3 * 1000);
               }catch (InterruptedException e){
    
    
                   // ignore
               }
               str.append("B");
           }
        });

        Thread t2 = new Thread(()->{
    
    
           synchronized (str){
    
    
               str.append("C");
               str.notify();
               str.append("D");
           }
        });

        t1.start();
        Thread.sleep(300);
        t2.start();

        t1.join();
        t2.join();
        System.out.println(str.toString());

    }
}

The question is what is printed by str.toString() in the end? , we can discuss it in the comment area...

Summarize

This article contains a lot of basic knowledge about multi-threading. If the reading partner is as weak as me, I suggest that you can make changes by hand and understand and memorize them at the same time. The effect will be much better. If there are loopholes in the description process, please criticize the pointer and write It was not easy to spend several hours writing this blog. Facing interviews is not the ultimate goal. A career in programming is a lifelong matter. The next article will introduce the knowledge points in thread concurrency.

Guess you like

Origin blog.csdn.net/qq_40187702/article/details/131585772