For Java multi-threading knowledge, this article is enough! (Super detailed)

Table of contents

1. Understanding Thread

1. Concept

 2. The first multi-threaded program

 (1) Observe threads

 3. Create a thread

2. Thread class and common methods

1. Common construction methods of Thread

2. Several common attributes of Thread

 3. Start a thread: start

4. Terminate a thread

(1) The programmer sets the flag manually

(2) Direct Thread class

 5. Wait for a thread

6. Get the reference of the current thread

7. Sleep the current thread

3. Thread status

1. Observe all states of threads 

NEW

RUNNABLE

BLOCKED 

WAITING 

TIMED_WAITING

TERMINATED

2. The meaning of thread status and status transfer

4. Risks brought by multi-threading - thread safety (the most important) 

1. Demonstration of thread safety issues 

2. Causes of thread safety issues

 3. Solve the previous thread insecurity problem

4. synchronized keyword

5. Problems caused by memory visibility

6. wait and notify

 7. Comparison between wait and sleep

5. Multi-threading case

1. Singleton mode

(1) Hungry Man Mode

(2) Lazy mode

(3) Solve the thread safety problem of lazy mode

(4) Memory visibility problem in lazy mode

2. Blocking queue

3. Timer

4. Thread pool

6. Common lock strategies

1. Optimistic locking VS pessimistic locking

2. Heavyweight lock VS lightweight lock

3. Spin lock VS suspend waiting lock

4. Read-write lock VS mutex lock

5. Fair lock VS unfair lock

6. Reentrant lock VS non-reentrant lock

7. Deadlock

8. Synchronized principle

1. Basic features 

2. Locking process 

 9. CAS

1. Concept 

2. Application of CAS

(1) Implement the atomic class

(2) Implement spin lock

 3. CAS’s ABA issues

10. Common classes of JUC (java.util.concurrent)

1、Collable interface

2、ReentrantLock

3. Atomic class

4. Semaphore

5、CountDownLatch

11. Collection class

(1) Using ArrayList in multiple environments

(2) Multi-threaded environment using queues

(3) Multi-threaded environment uses hash table

(a)Hashtable

(b)ConcurretnhashMap


1. Understanding threads (Thread )

1. Concept

Multi-process has already achieved the effect of concurrent programming very well.

But there is an obvious disadvantage: the process is too heavy

1. Consume more resources

2. Slower speed

It's okay if processes are created and destroyed infrequently. Once processes need to be created and destroyed on a large scale, the overhead will be relatively high.

The overhead is mainly reflected in the process of allocating resources to processes.

So the smart programmer thought of a way: when creating a process, can he only allocate a simple pcb instead of allocating subsequent memory and hard disk resources?

So there is a lightweight process, that is, a thread (Thread)

The thread only creates a pcb, but does not allocate subsequent resources such as memory, hard disk, etc.

Threads are created to perform some tasks, but performing tasks requires consuming these hardware resources.

So, what we create is still a process, but when we create a process, all the resources are allocated, and the threads created subsequently are kept inside the process (the relationship between processes and threads can be considered as processes containing threads )

New threads in subsequent processes directly reuse the resources created here in the previous process.

In fact, a process must contain at least one thread

So the initially created one can be regarded as a process containing only one thread (the creation process at this time needs to allocate resources, and the creation overhead of the first thread may be relatively large at this time)

But if you create a thread in this process later, you can omit the process of allocating resources. The resources are already there.

Multiple threads in a process jointly reuse various resources (memory, hard disk) in the process, but these threads are scheduled independently on the CPU

Therefore, threads can not only achieve the effect of "concurrent programming", but also perform it in a relatively lightweight way.

Threads are also described through PCB

On Windows, processes and threads are described by different structures, but on Linux, Linux developers reuse the pcb structure to describe threads.

At this time, one pcb corresponds to one thread, and multiple pcbs correspond to one process.

The memory pointer and file descriptor table in the PCB. In multiple PCBs of the same process, the contents of these two fields are the same, but the context status, priority, accounting information...support scheduling attributes, Each of these pcbs is different

So there are these two sentences:

Process is the basic unit of allocation by the operating system

Threads are the basic unit of scheduling and execution by the operating system.

Compared with multi-process programming, multi-thread programming does have advantages: it is more lightweight and faster to create and destroy.

But there are also disadvantages: not as stable as the process

In Java, when doing concurrent programming, you still need to consider multi-threading

Whether it is "multi-threading" or "multi-process", they are essentially implementation models of "concurrent programming". In fact, there are many other implementation models of "concurrent programming". 

Talk about the differences and connections between processes and threads

1. Processes include threads, all to achieve "concurrent programming". Threads are more lightweight than processes

2. The process is the basic unit of resource allocation by the system, and the thread is the basic unit of system scheduling and execution. When creating a process, the work of allocating resources is done. If you create a thread later, you can directly share the previous resources.

3. Processes have independent address spaces and do not affect each other. The independence of processes improves the stability of the system. Multiple threads share an address space. Once a thread throws an exception, it may Causes the entire process to end abnormally, which means that multiple threads can easily affect each other. 

4. There are some other points, but the above three core points are very important.

Threads are more lightweight, but they are not without creation costs. If threads are created/destroyed very frequently, the overhead cannot be ignored.

At this time, the smart programmer thought of two more ways;

1. "Lightweight thread", that is, coroutine/fiber

This thing is not yet built into the Java standard library, but there are some third-party libraries that implement coroutines.

2. "Thread Pool"

Pool (poll) is actually a very classic way of thinking in computers: don’t release some resources to be released in a hurry, but put them into a pool for subsequent use; when applying for resources, you should also apply for them in advance. It is easy to apply for resources and put them in a "pool", which is more convenient for subsequent applications.


 2、第一个多线程程序

线程本身是操作系统提供的概念,操作系统也提供了一些API来供程序员使用(Linux,pthread)

Java中就把操作系统的API又进行了封装,提供了thread类

先创建一个类,让它继承 Thread,然后再重写一下 run 方法,这里的run 就相当于线程的入口 方法,线程具体跑起来之后,要做什么事,都是通过这个 run 来描述的

不仅仅是要创建出类,还需要调用它,让它执行起来 ,此处的 start 就是在创建线程(这个操作就会在底层调用 操作系统 提供的API ,同时就会在操作系统内核里创建出对应的pcb结构,并且加入到对应链表中)

此时,新创建出来的线程就会参与到cpu的调度中,这个线程接下来要执行的工作,就是刚刚上面刚刚重写的run 方法 

java.lang 这个包比较特殊,这里的类,不需要手动 import 直接默认就能使用

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


public class Demo1 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
    }
}

也就是说每当点击运行程序的时候,就会先创建出一个Java进程,这个进程就包含了至少一个线程,这个线程也叫做主线程,也就是负责执行 main 方法的线程

如果将上面的代码改成 mythread.run();,我们会发现也能正确执行,但是它和 start 是有区别的

run 只是上面的入口方法(普通的方法),并没有调用系统API,也没有创建出真正的线程来

现在我们调整代码成这个样子:

class Mythread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread!");
        }
    }
}


public class Demo1 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
        //mythread.run();

        while(true){
            System.out.println("hello main!");
        }
    }
}

此时,在 main 方法中有一个 while 循环,在线程的 run 中也有一个 while 循环,这两个循环都是 while(ture) 死循环。

使用 start 的方式执行,此时这两个循环都在执行,并且交替打印结果,这是因为两个线程分别执行自己的循环,这两个线程都能够参与到 cpu 的调度中,这两个线程并发式的执行

主线程和新线程式并发执行的关系,全看操作系统怎么调度

If it is written in run mode, it means that no new thread has been created, and it is still within the original main thread. Only after the while in run is executed, can it return to the calling location and execute later. Since the while in run It is an infinite loop, so the while in the main thread has no chance to execute

Each thread is an independent execution flow

Each thread can execute a piece of code

There is a concurrent execution relationship between each thread 

Sometimes, we will add a sleep to run 

Thread.sleep(1000);

We have unified these differences in Java. Thread.sleep is equivalent to encapsulating the above system functions. If it is running on the Windows version of jvm, the bottom layer is to call Windows' Sleep. If it is running on Linux version of the jvm, the bottom layer is to call Linux sleep

sleep is a static method of Thread class

For this exception, we currently choose to use the try-catch method to solve it. Then if we run it again, it will become a rhythmic run.


Note: This is not a very strict alternation, and the order may also be reversed.

This is because the two threads will enter the blocking state after sleeping. Whenever the time is up, the system will wake up the two threads and resume the scheduling of the two threads. When both threads are awakened, The order of scheduling can be regarded as "random"

When the system schedules multiple threads, it does not have a very clear order, but schedules them in this "random" way. This "random" scheduling process is called "preemptive execution" 

The randomness we usually talk about actually implies the setting of "equal probability", but the randomness here at least looks random, but in fact it does not necessarily guarantee an equal profile.


 (1) Observe threads

After we create a thread, we can also directly observe it in some ways. 

1. Use idea’s debugger directly

When running, choose to execute according to the debugging method.

 

2、jconsole

Official debugging tools provided in jdk

We can find jconsole in the bin directory according to the path of jdk download before.

Then run the program first, and then open jconsole, which lists all the Java projects running on the system (jconsole is also a program written in Java)

 Then select the thread connecting, and then select the thread tag page

At this time, all threads are displayed in the lower left corner. All threads in the current process are listed here, not only the main thread and the threads created by yourself, but also some other threads. The remaining threads are all in the JVM. Comes with it and is responsible for completing some other aspects of work

After clicking on the thread, you can view the detailed information of the thread

Stack trace: Thread’s call stack, showing the calling relationship between methods

When the program is stuck, we can check the call stack of each thread here, and we can roughly know in which code the stuck situation occurred.


 3. Create a thread

 In Java, there are many ways to create threads through the Thread class.

1. Create a class, inherit Thread, and override the run method

2. Create a class, implement Runnable, and override the run method

Implementation refers to implementing interface 

In Java, interface is usually described using an adjective part-of-speech word.

package thread;

import java.util.TreeMap;

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

//使用 Runnable 的方式创建线程
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

the difference:

Thread Here, the work to be completed is directly put into the run method of Thread.

Runnable here is separated. The work to be completed is put into Runnable, and then Runnable and Thread are allowed to cooperate.

Further decouples the tasks to be performed by the thread from the thread itself.

3. Inherit Thread, rewrite run, based on anonymous inner class

1. First create a subclass. This subclass inherits from Thread, but this subclass has no name (anonymous). On the other hand, this class was created in Demo3

2. In the subclass, the run method is rewritten again

3. Create an instance of the subclass and use the reference t to point to

//通过匿名内部类的方式,创建线程

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello Thead!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();
        while(true){
            System.out.println("hello main!");
            Thread.sleep(1000);
        }

    }
}

4. Implement Runnable, rewrite run, based on anonymous inner class

1. Created a Runnable subclass (class, implements Runnable)

2. Rewritten the run method

3. Create an instance of the subclass and pass the instance to the Thread constructor 

) corresponds to the end of the Thread constructor. The entire new Runnable section is constructing a Thread parameter.

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();
        while(true){
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

5. Use lambda expression to express the content of run (recommended)

 A lambda expression is essentially an "anonymous function"

Such anonymous functions can mainly be used as callback functions.

The callback function does not require us programmers to actively call it, but is automatically called at the right time.

Scenarios where "callback functions" are often used:

1. Server development: The server receives a request and triggers a corresponding callback function.

2. Graphical interface development: A certain user operation triggers a corresponding callback

The callback function here is actually executed after the thread is successfully created.

Similar to lambda, there is no new language feature in essence, but the functions that can be implemented in the past are written in a more concise way. 

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();

    }
}

There are not only the above five methods for creating threads, but also methods based on Callable and thread pool . These are the more common writing methods at present.


2. Thread class and common methods

Thread is the spokesperson for Java threads. That is to say, a thread in the system corresponds to a Thread object in Java . Various operations around threads are carried out through Thread. 

1. Common construction methods of Thread

Java names threads: Thread-0, which is less readable. If there are dozens of threads in a program with different functions, then we can name the threads when creating them. 

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"myThread");

        t.start();

    }
}

 We name the thread "Mythread" here. When the code runs, you can find the thread in jconsole.

We will find that at this time, there is no main in the thread. This is because main has finished executing. 

The thread is created through start, and a thread entry method is executed (for the main thread, it is main; for other threads, run / lambda)


2. Several common attributes of Thread

ID:getId()

The identity of the thread (the identity set for the thread in the JVM)

A person can have many names, and a thread can also have several identities.

The JVM has an identity, the pthread library (the API of the operating system provided by the system to programmers), and an identity. In the kernel, the PCB for threads also has an identity. These identities are independent of each other .

Status, priority:

There are certain differences between the thread status in Java and the operating system.

Setting/getting the priority is not very useful, because the system kernel is mainly responsible for thread scheduling.

Whether background thread:

Background thread/daemon thread: background thread does not affect the end

Foreground thread: Will affect the end of the process. If the foreground thread has not finished executing, the process will not end.

If all the foreground threads in a process have finished executing, even if the background threads have not finished executing, they will exit along with the process.

The thread created is a foreground thread by default. You can set the background process through setDeamon().

//后台线程 和 前台线程

public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello Thread!");

            }
    });

        //设置成后台线程
        t.setDaemon(true);
        t.start();

    }
}

Is it alive?

Indicates whether the thread corresponding to the Thread object (in the system kernel) is alive

In other words, the life cycle of the Thread object is not completely consistent with the threads in the system! !

Generally, the Thread object is created first, and start is manually called before the kernel actually creates the thread.

When it dies, it may be that the Thread object ends its life cycle first (this object is not referenced)

It is also possible that the Thread object is still there, and the thread in the kernel finishes executing the run first and then ends.


 3. Start a thread: start

In the system, actually create a thread:

1. Create the PCB

2. Add the PCB to the corresponding linked list

This is done by the system kernel 

What is the operating system kernel?

Operating system = kernel + supporting programs

The kernel contains the core functions of a system:

1. Right, manage various hardware devices

2. Correct, provide a stable operating environment for various programmers

The most critical thing is: call the system’s API to complete the thread creation work

The execution of the start method itself is completed in an instant. After calling start, the code will immediately continue to execute the subsequent logic of start.


4. Terminate a thread

A thread is terminated after the run method is executed.

The termination here is to find a way to complete the run as soon as possible.

Under normal circumstances, there will be no situation where the thread suddenly disappears before the run is completed. If a thread completes half of the work and suddenly disappears, sometimes it will cause some problems.

(1) The programmer sets the flag manually

Through this manually set flag

//线程终止

public class Demo8 {
    public static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(!isQuit){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();

        //主线程这里执行一些其它逻辑之后,让 t 线程结束
        Thread.sleep(3000);

        //这个代码就是在修改前面设置的标志位
        isQuit = true;
        System.out.println("把线程终止");
    }
}

Through the above code, the t thread is indeed ended. sleep(3000) is actually printed 4 times, which is normal.

The main reason is that there are errors in the sleep operation. Once the actual sleep time is more than 3000, the t thread may print out the fourth log, so printing 3 or 4 times is possible.

Why isQuit can be accessed when written as a member variable, but an error will be reported when written as a local variable?

 Lambda expression can capture external variables

Since the execution time of lambda expression is later, this will lead to the possibility that the local variable isQuit has been destroyed when the lambda is actually executed later?

In other words, the main thread ends first, and isQuit is destroyed at this time. When the lambda is actually executed, isQuit has been destroyed.

This situation exists objectively. It is obviously inappropriate to let lambda access a variable that has not been destroyed.

lambda introduces a mechanism such as "variable capture"

It seems that the lambda is directly accessing the external variables internally. In fact, it is essentially copying the external variables into the lambda (this can solve the life cycle problem just now)

However, variable capture has a limitation

Variable capture requires that the captured variable must be final 

When we delete isQuit = true;, no error will occur, but a prompt will be given:

This means that although the final modification is not used, the variable is not modified in the code. At this time, the variable can also be regarded as final. 

If this variable wants to be modified, variable capture cannot be performed at this time.

So why do we need to set this up?

Java implements "variable capture" by copying . If the external code wants to modify this variable, a situation will occur: the external variable has changed, but the internal variable has not changed (ambiguity is prone to occur)

相比之下,其他语言(JS)采取了更激进的设计:也有变量捕获,不是通过复制的方式实现,而是直接改变外部变量的生命周期,从而保证lambda在执行的时候肯定能访问到外部的变量(此时 js 中的变量捕获就没有 final 的限制)

如果写作成员变量,就不是触发 “变量捕获” 的机制了,而是 “内部类访问外部类成员” ,本身就是ok 的,也就不用关心是否是 final 的问题了


(2)直接 Thread 类

Thread 类给我们提供好了现成的标志位,不用我们手动设置标志位

//线程终止,使用 Thread 自带的标志位

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread() 其实就是 t
            //但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        
        //把上述的标志位设置成 true
        t.interrupt();
    }
}

当代码运行起来后,我们会发现,程序并没有按照我们的想法去执行

根据提示,我们可以将其理解成: t 线程正在 sleep ,然后被 interrupt 给唤醒了

如果是手动设置标志位,此时是没法唤醒 sleep 的 

一个线程,可能是在正常运行,也可能是在 sleep,如果这个线程在 sleep 过程中,是否应该把它唤醒呢?

还是得唤醒的

因此,当线程正在sleep过程中,其它线程调用 interrupt 方法,就会强制使sleep抛出一个异常,sleep就会立即被唤醒了(假设你设定 sleep(1000),虽然才过去了 10 ms,没到1000 ms,也会被立即唤醒)

但是 sleep 在被唤醒的同时,会自动清除前面设置的标志位!!!

此时,如果想继续让线程结束,直接就在 catch 中加一个 break 即可

//线程终止,使用 Thread 自带的标志位

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread() 其实就是 t
            //但是 lambda 表达式是在构造 t 之前就定义好了的,编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    break;
                }
            }
        });

        t.start();
        Thread.sleep(3000);

        //把上述的标志位设置成 true
        t.interrupt();
    }
}

当 sleep 被唤醒之后,程序员接下来可以有以下几种操作方式:

1、立即停止循环,立即结束线程

2、继续做点别的事情,过一会再结束线程(catch中执行别的逻辑,执行完了再break)

3、忽略终止的请求,继续循环(不写break)

 

前者是静态方法,判定标志位会在判定的同时被清除标志位了

后者是成员方法,因此更推荐使用后者 


 5、等待一个线程

Multiple threads execute concurrently, and the specific execution process is scheduled by the operating system.

The scheduling process of the operating system is "random"

Therefore, we cannot determine the order in which threads execute

Waiting for threads is a means of planning the order in which threads end.

Waiting for the end is to wait for the run method to complete execution.

Blocking: Let the code not continue to execute temporarily (the thread will not participate in scheduling on the CPU for the time being)

Threads can also be blocked through sleep, but this blocking has a time limit.

The blocking of join is "waiting to death"

Can join be awakened by interrupt?

Yes, sleep, join, wait... After blocking, it is possible to be awakened by the interrupt method. These methods will automatically clear the flag bit after being awakened (similar to sleep)

public class Demo10 {
    public static void main(String[] args) {
        Thread b = new Thread(() -> {
            for(int i = 0;i < 5;i ++){
                System.out.println("hello B!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("b 结束了");
        });
        
        Thread a = new Thread(() -> {
            for(int i = 0;i < 3;i ++){
                System.out.println("hello A!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                //如果 b 此时还没执行完毕, b.join 就会产生阻塞情况
                b.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("A 结束了");
        });

        b.start();
        a.start();

    }
}

6. Get the reference of the current thread

We are very familiar with this method

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

7. Sleep the current thread

It is also a group of methods that we are more familiar with. One thing to remember is that because thread scheduling is uncontrollable, this method can only ensure that the actual sleep time is greater than or equal to the sleep time set by the parameters.

The parameter is ms as the unit 

But sleep itself has certain errors

Set sleep(1000), it does not necessarily mean sleeping for 1000ms exactly! ! (Thread scheduling also takes time)

sleep(1000) means that the thread will return to the "ready state" after 1000 ms. At this time, it can be executed on the CPU at any time, but it may not be specified immediately.

Because of the characteristics of sleep, a special technique was born: sleep(0)

Let the current thread give up the CPU and equip the next round of scheduling (generally not involved in background development)

The effect of the yield method is the same as that of sleep(0) 

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

3. Thread status

1. Observe all states of threads 

 Java provides a total of the following six states:

NEW

The Thread object has been created, but the Thread method has not been called yet.

This can be verified with code: 

//线程的状态

public class Demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
        while(true){
           // System.out.println("hello Thread!");
        }
        });

        System.out.println(t.getState());

        t.start();

    }
}

RUNNABLE

 The ready state can be understood as two situations:

(1) The thread is running on the cpu

(2) Threads are queued here and can be executed on the CPU at any time

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(true){

            }
        });

        System.out.println(t.getState());

        t.start();

        //t.join();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

BLOCKED 

Because the lock creates a blocking

WAITING 

Blocking occurs because wait is called.

TIMED_WAITING

Because  sleep blocks 

If you use the join version with timeout, TIMED_WAITING will also be generated.

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        System.out.println(t.getState());

        t.start();

        //t.join();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

TERMINATED

work completed status 

//线程的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println(t.getState());

        t.start();

        t.join();
        System.out.println(t.getState());
    }
}

2. The meaning of thread status and status transfer

 If you use a simpler and easier to understand way to understand the transition between states, you can use the following figure to represent it:


4. Risks brought by multi-threading - thread safety (the most important) 

1. Demonstration of thread safety issues 

Let’s first look at this code and its results:

//线程安全问题演示

class Counter{
    public  int count = 0;

    public void increase(){
        count++;
    }
}

public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

Logically speaking, the running result at this time should be 100000, but the following results appeared:

Logically speaking, two threads perform cyclic self-increment for a variable, each incrementing it 50,000 times. The expected result should be 10,000,000, but in fact it is not. And when we run the program multiple times, we will find that the results of each operation are different. no the same 

Under multi-threading, we found that bugs caused by multi-threaded execution are collectively referred to as "thread safety issues". If a certain code has no problem executing under a single thread or multiple threads, it is called " Thread safe", the opposite is called "thread unsafe"

So whether threads are safe depends on whether there are bugs in the code, especially bugs caused by multi-threading. 

count++;

This operation is essentially three steps:

1. Load the data in the memory into the CPU register (load).

2. Add +1 to the data in the register (add)

3. Write the data in the register back to the memory (sava).

If the above operations are performed concurrently by two threads or more threads, problems may occur!

In order to better represent the execution process of this count, we draw a timeline to better understand

Since the scheduling order of these two threads is uncertain here, there will also be differences in the relative order of these two sets of operations.

                                                                                                                       

Although it is incremented twice, since the two threads are executed concurrently, the intermediate results of the operation may be overwritten in a certain execution order.

During these 50,000 cycles, how many times are the ++ of the two threads serial, and how many times will the overwriting result appear? It is uncertain because the thread scheduling is "random "

Here, the result here will cause problem i, and the error value obtained must be less than 10w

Many codes involve thread safety issues, not just count++


2. Causes of thread safety issues

1. [Root cause] The scheduling order among multiple threads is "random", and the operating system uses a "preemptive" execution strategy to schedule threads.

Different from single-threading, under multi-threading, the execution order of the code has produced more changes. In the past, it was only necessary to consider that the code was executed in a fixed order and it was executed correctly. Now we need to consider that under multi-threading, under N execution orders, the results of code execution must be correct.

The current mainstream operating systems all use this kind of preemptive execution.

2. Multiple threads modify the same variable at the same time, which can easily cause thread safety issues.

One thread modifies a variable, multiple threads read the same variable, and multiple threads modify multiple variables. All three situations are fine.

This condition actually talks about the problem of code structure . We can avoid this situation by adjusting the code structure. This is also the most important way to solve the thread safety problem.

3. The modifications made are not "atomic" 

If the modification operation can be completed in an atomic manner, there will be no thread safety issues at this time.

4. Thread safety issues caused by memory visibility

5. Thread safety issues caused by instruction reordering 


 3. Solve the previous thread insecurity problem

 Locking is equivalent to packaging a set of operations into an "atomic" operation.

The atomic operation of the transaction mainly relies on rollback, and the atomic operation here is "mutually exclusive" through locks: when my thread is working, other threads cannot work.

The lock in the code allows multiple threads to use this variable at the same time.

Facing the previous code, the locking operation is performed when counting ++ is performed.

In Java, a synchronized keyword is introduced 

When we lock a method, it means that when entering the method, it will be locked (locked).   When leaving the method, it will be unlocked (unlocked). 

After t1 locks, t2 also tries to lock. At this time, t2 will block and wait. This blocking will continue until t1 releases the lock, and t2 can successfully lock. 

The blocking wait of t2 here postpones t2's ++ operation for count until later. Only when t1 completes count++ can t2 actually perform count++, turning "interleaved execution" into "serial execution"

So at this time, let’s take a look at the effect after locking:

Through the locking operation, concurrent execution is turned into serial execution. So does multi-threading still have meaning at this time?

Our current thread does not just do count++. The increase method becomes serial because of the lock. However, the for loop above is not locked because it does not involve thread safety issues. The variable i operated on in the for loop is a local variable on the stack. Two threads have two independent stack spaces, which are completely different variables.

In other words, i in the two threads is not the same variable. If the two threads modify two different variables, there is no thread safety problem, and there is no need to lock.

Therefore, for these two threads, part of the code is executed serially and part is executed concurrently, which is still more efficient than pure serial execution.


4. synchronized keyword

This keyword is the locking method (keyword) provided by Java.

This is often done with code blocks:

1. Lock when entering the code block

2. Unlock after exiting the code block

Synchronized locking and unlocking is actually expanded with the "object" dimension.

The purpose of locking is to use resources for mutual exclusion (mutually exclusive modification variables)

When using synchronized, a specific object is actually specified for locking. When synchronized directly modifies the method, it is equivalent to locking this (the modified method is equivalent to the simplification of the above code)

If two threads lock the same object, lock competition/lock conflict will occur (one thread can lock successfully, and the other thread blocks and waits)

If two threads lock different objects , there will be no lock competition, and there will be no blocking and waiting and a series of operations.

Like the code just now, if two threads lock different objects, there will be no blocking wait, and the two threads will not be allowed to perform count++ in a serial manner, and there will still be thread safety. question

For example, the following code: 

//线程安全问题演示

class Counter{
    public  int count = 0;
    public Object locker = new Object();

     public void increase(){
        synchronized (this){
            count++;
        }
    }

    public void increase2(){
         synchronized (locker){
             count++;
         }
    }
}

public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase();
            }
        });

        Thread t2 = new Thread(() ->{
            for(int i = 0;i < 50000;i++){
                counter.increase2();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

But if you change the locking object of the first increase to locker, it will still be the same object at this time, and blocking waiting will occur, which will naturally solve the thread safety problem.

It doesn’t matter which object is locked; the most important thing is whether the two threads lock the same object.

If in the following code, one thread is locked and the other thread is not locked, will there be a thread safety problem at this time? 

There is still a problem! ! !

A slap in the face can't make a sound, unilateral locking means no locking. It only makes sense to have multiple threads locking the same object!

It doesn’t matter which object you lock. You don’t have to write this in (). It doesn’t matter what you write. What matters is whether the locking operations of multiple threads are for the same object. The object written in synchronized can be one, but In fact the member of the operation can be another

If you use synchronized to modify a static method, it is equivalent to using synchronized to lock the class object.

The complete information of a class is initially in the .java file and will be further compiled into a .class. When jvm loads the .class, it will parse the contents and construct an object in memory, which is the class object of the class.

In synchronized, it makes no difference whether you write a class object or an ordinary object in (). synchronized does not care what the object is, it only cares whether the two threads lock the same object.


5. Problems caused by memory visibility

Now there is this piece of code: 

 t1 always performs a while loop, and t2 allows the user to input an integer through the console as the value of isQuit

When the user input is still 0, the t1 thread continues to execute. When the user input is non-0, the thread should end the loop.

//内存可见性

import java.util.Scanner;

public class Demo13 {

    public static int isQuit = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            while(isQuit == 0){

            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入 isQuit 的值");
            isQuit = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

 But when we ran the program, we found a problem: when we entered a non-0 value, the value of isQuit has been modified, but the t1 thread is still executing.

When the program is compiled and run, the Java compiler and jvm may perform some "optimizations" on the code.

Compiler optimization essentially relies on the code to intelligently analyze, judge and adjust the code you write. In most cases, this adjustment is OK and can ensure that the logic remains unchanged. However, if you encounter multi-threading, this Errors may occur during optimization, causing the original logic in the program to change.

This conditional judgment essentially makes two instructions:

1. load (read memory), the memory reading operation speed is very slow

2. jcmp (compare and jump), this comparison is a register operation and is extremely fast

At this time, the compiler/JVM discovered that in this logic, the code needs to read the same memory value repeatedly and quickly, and the value of this memory is still the same every time it is read out, so the compiler makes a bold decision Decision: directly optimize the load operation, no longer load in the future, directly compare the data in the register 

However, it did not expect that the programmer modified the value of isQuit in another thread. At this time, the compiler could not accurately determine whether and when the t2 thread would be executed, so a misjudgment occurred.

Although the value of isQuit in the memory is changed here, the value of isQuit is not repeatedly read in another thread, so the t1 thread cannot sense the modification of t2, and the above problem occurs.

The volatile keyword is used to make up for the above defects.

After using volatile to modify a variable, the compiler will understand that this variable is "volatile" and cannot optimize the read operation into the register in the above way. (The compiler will disable the above optimization), so it can Ensure that t1 can always read the data in the memory during the loop.

We can see that after adding volatile to the source code, the t1 thread can terminate normally.

Volatile essentially guarantees the memory visibility of the variable (prohibiting the read operation of the variable from being optimized into the read register) 

Compiler optimization is actually a metaphysical issue. When to optimize and when not to optimize, we can't figure out the rules.

If the above code is slightly modified, the above optimization may not be triggered, for example:

When we add sleep, sleep will greatly affect the speed of the while loop. If the speed is slow, the compiler will not continue to optimize. At this time, even if volatile is not added, the memory changes can be sensed in time. Got it

Supplement: Working memory (work memory) is not the memory in the von Neumann architecture we are talking about, but the CPU register + CPU cache, collectively referred to as "working memory"

Memory visibility issues:

1. Compiler optimization

2. Memory model

3. Multi-threading

volatile guarantees memory visibility, not atomicity! ! !


6. wait and notify

wait and notify are also important tools in multi-threading

Multi-thread scheduling is "random". Many times, we hope that multiple threads can be executed in the order we specify to complete the cooperation between threads.

wait and notify are important tools for coordinating thread order.

These two methods are methods provided by Object. That is to say, you can use wait and notify for any object.

Now, when we try to use the wait method, the compiler prompts:

Meaning: When wait causes a thread to block, you can use the interrupt method to wake up the thread and interrupt the blocking state of the current thread.

Then we throw the exception, execute the code again, and find that an exception occurred:

Meaning: Illegal lock status exception

The lock status is nothing more than two: locked and unlocked

So why does an error occur?

When wait is executed again, it will do three things:

1. Unlock, object.wait will try to unlock the object object

2. Blocking and waiting

3. When awakened by other threads, it will try to re-lock. If the lock is successful, wait is executed and other logic continues to be executed.

The prerequisite for wait to unlock is to lock it first, but we haven't even locked it yet. 

Core idea: lock first, then wait in synchronized

When we lock this code and then run the program, we will find that it has successfully entered the blocking state, and the wait here will block until other threads notify.

The main purpose of wait and notify is to arrange the execution order between threads. One of the most typical scenarios is to effectively avoid "thread hunger/starvation"

Demo of wait and notify:

//wait 和 notify
public class Demo15 {
    //使用这个锁对象来负责加锁,wait,notify
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            while(true){
                synchronized (locker){
                    System.out.println("t1 wait 开始");
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("t1 wait 结束");
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker){
                    System.out.println("t2 notify 开始");
                    locker.notify();
                    System.out.println("t2 notify 结束");
                }
            }
        });
        t2.start();

    }
}

Execution effect:

A few notes:

1. If you want notify to wake up wait smoothly, you need to ensure that wait and notify are called using the same object.

2. Both wait and notify need to be placed in synchronized. Although notify does not involve "unlocking operation", Java requires notify to be placed in synchronized.

3. If another thread is not in the wait state when notify is performed, notify is equivalent to "shooting in vain" and will not have any side effects.

There may be i threads. For example, there can be N threads for wait, and one thread is responsible for notify. At this time, the notify operation will only wake up one thread. Which thread is woken up is random.

notifyAll can wake up all threads in waiting

If you want to wake up a specified thread, you can let different threads use different objects to wait. If you want to wake up someone, you can use the corresponding object to notify


 7. Comparison between wait and sleep

sleep has a clear time. It will naturally wake up when it reaches the time. It can also wake up in advance. Just use interrupt.

By default, wait is in a dead-waiting state, waiting until other threads notify. Wait can also be awakened in advance by interrupt.

notify can be understood as a smooth wake-up. After the wake-up, the thread needs to continue working, and it will enter the wait state later.

interrupt tells the thread that it is about to end, and then the thread will enter the finishing work

wait also has a version with a timeout (similar to join)

Therefore, coordinate the execution order between multiple threads, and of course give priority to using wait and notify instead of sleep


5. Multi-threading case

1. Singleton mode

Singleton pattern is a design pattern

Design patterns are the programmer's chess game, which introduces many typical scenarios and how to deal with typical scenarios.

Scenarios corresponding to the singleton mode: Sometimes, we hope that some objects will have only one instance (object) in the entire program, which means that they can only be new once.

It is unreliable to rely on people for guarantee. Here we need to rely on the compiler to perform a more stringent check and find a way to make the code only new the object once. If new is tried multiple times, an error will be reported directly.

In Java, there are many ways to achieve this effect. Here we mainly introduce two :

1. Hungry mode (urgent) program starts, and an instance is created immediately after the class is loaded.

2. Lazy mode (delay) creates an instance when it is used for the first time, otherwise it will not be created if possible.

for example:

The compiler opens a file. Suppose there is a very large file. Some compilers will load all the contents into the memory at once (hungry man) , and some compilers will only load part of the contents, and the other parts will be paged by the user. When the time comes, load it again when you need it (lazy guy)

(1) Hungry Man Mode

With static, it represents class attributes. Since the class object of each class is a singleton, the attributes of the class object (static) are also singletons.

When we make a restriction on the code: prohibit others from new this instance, that is, change the construction method to private

The actual execution of this code is when the Singleton class is loaded by the jvm. The Singleton class will be loaded when the jvm is used for the first time, not necessarily when the program starts.                                                         

 At this time, this code compiles and reports an error.

At this time , there is only one . By writing this way, you can effectively implement the singleton mode.

So, if the constructor is set to private, then it will definitely not be able to be called from outside?

Using reflection, you can indeed create multiple instances in the current singleton mode. However, reflection itself is an "unconventional" programming method. During normal development, it should be used with caution. Abuse of reflection will bring great risks: it will make the code more abstract and difficult to maintain.

//单例模式(饿汉模式)
class Singleton{
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return  instance;
    }

    //做出一个限制:禁止别人去 new 这个实例
    private Singleton(){

    }
}

public class Demo16 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        //Singleton s3 = new Singleton();  会报错

        System.out.println(s1 == s2);
        //System.out.println(s1 == s3);
    }
}

(2) Lazy mode

class Singletonlazy{
    private static Singletonlazy instance = null;

    public static Singletonlazy getInstance(){
        if (instance == null){
            instance = new Singletonlazy();
        }
        return instance;
    }

    private Singletonlazy(){

    }
}
//单例模式(懒汉模式)

public class Demo17 {
    public static void main(String[] args) {
        Singletonlazy s1 = Singletonlazy.getInstance();
        Singletonlazy s2 = Singletonlazy.getInstance();
        System.out.println(s1 == s2);
    }
}

 The front is all foreshadowing, let’s get to the point:

Among the two modes just mentioned, which one is thread-safe, that is, when multiple threads call getInstance, which code is thread-safe (will not have bugs)

For the hungry man mode : in multi-threads, there is no problem in reading the content of instancece and reading the same variable in multiple threads!

For lazy mode : This is where problems arise

At this time, SingletonLazy will be new and create two objects , and it will no longer be a singleton.

At this time, there will be questions: Doesn't the second new operation modify the reference of the original instance? Wasn't the object that was previously new immediately recycled? Isn't there still one object left in the end?

Note: The overhead of new an object may be very large. If you new it multiple times, it will cost a lot of overhead.

Conclusion: Lazy mode code is not thread-safe! ! !


(3) Solve the thread safety problem of lazy mode

We can solve this problem by locking, so we have the following code:

However, this way of writing is wrong and  does not solve the above thread safety problem . This is because the operation itself is atomic, and locking it again does not make any substantial changes.

So how to modify it?

Add the lock to the outside;

    public static Singletonlazy getInstance(){
        synchronized (Singletonlazy.class){
            if (instance == null){
                instance = new Singletonlazy();
            }
        }
        return instance;
    }

In this way, the above problems are solved, but there are still other problems:

Locking is a relatively expensive operation, and locking may cause blocking waiting.

The basic principle of locking should be: do not lock unless necessary , and do not lock without thinking. If locking is done without thinking, the execution efficiency of the program will be affected. 

This way of writing means that every subsequent call to GetInstance must be locked, but it is not necessary .

The thread in lazy mode is not safe. The problem mainly occurs when the object is new for the first time. Once the object is new, there will be no problem if getInstance is called later.

In other words, once an object is new, subsequent if conditions cannot enter, and there will be no modification operations involved, only read operations.

In this way of writing, locking is performed on both the first call and subsequent calls. In fact, subsequent calls do not need to be locked. Here, the places that should not be locked are locked, which will greatly affect the efficiency of the program.

So how to solve such a problem?

First determine whether to lock, and then decide whether to actually lock.

    public static Singletonlazy getInstance(){
        // instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
        if (instance == null){
            synchronized (Singletonlazy.class){
                if (instance == null){
                    instance = new Singletonlazy();
                }
            }
        }

        return instance;
    }

Is it reasonable to write the same if condition twice?

Very reasonable! ! !

This is because the locking operation may be blocked, and it is impossible to determine how long it will be blocked. There may be a very long time interval between the second condition and the first condition. During this long time interval, other threads may change the instance.

The first condition: determine whether to lock

Second condition: Determine whether to create an object


(4) Memory visibility problem in lazy mode

Assume that there are two threads executing at the same time. After modifying the instance, the first thread ends the code block and releases the lock. The second thread can acquire the lock and recover from the blocking. Then the second thread Will a read operation performed by two threads be able to read the value modified by the first thread? 

This is the memory visibility problem 

Here, we need to add a volatile to the instance 

Whether there will be a memory visibility problem here, we have only analyzed that there may be such a risk, but it is uncertain whether the compiler will actually trigger optimization in this scenario.

This is actually fundamentally different from the previous code: the previous memory visibility was read repeatedly by one thread, but now it is read repeatedly by multiple threads. It is unknown whether the previous optimization will be triggered at this time, but adding volatile is more secure . practice

Adding volatile here has another purpose: to avoid the reordering of instructions for assignment operations here

Instruction reordering is also a means of compiler optimization: while ensuring that the original execution logic remains unchanged, the code execution order is adjusted to improve the execution efficiency after adjustment.

If single-threaded, such reordering is usually fine. If multi-threaded, problems may arise.

This operation involves the following three steps: 

1. Create a memory space for the object and obtain the memory address

2. Call the constructor method on the space to initialize the object

3. Assign the memory address to the instance reference

This may involve instruction reordering , and 123 may become 132. If it is a single thread, it does not matter whether 2 or 3 is executed first at this time, but it is not necessarily the case for multi-threads. 

Or assume that there are two threads executing at the same time

Assume that the first thread is executed in the order of 1 3 2, and after executing 3 and before 2, a thread switch occurs. At this time, before there is time to initialize the object, it is scheduled to other threads.

Next, the second thread executes and determines the instance! = null, so instance is returned directly, and some attributes or methods in instance may be used later, but the object obtained here is an incomplete and uninitialized object . Use attributes/methods for this incomplete object. , some situations may arise

After adding volatile to the instance, the assignment operation performed on the instance will not cause the above instruction reordering. It must be executed in the order of 123 instead of 132.

class Singletonlazy{
    private static volatile Singletonlazy instance = null;

    public static Singletonlazy getInstance(){
        // instance 如果为空,就说明是首次调用,需要加锁,如果是非null,就说明是后续调用,不用加锁
        if (instance == null){
            synchronized (Singletonlazy.class){
                if (instance == null){
                    instance = new Singletonlazy();
                }
            }
        }

        return instance;
    }

    private Singletonlazy(){

    }
}

2. Blocking queue

Blocking queue has blocking function:

1. When the queue is full, if you continue to enter the queue, blocking will occur until other threads take elements from the queue.

2. When the queue is empty, if you continue to dequeue, blocking will occur until other threads add elements to the queue.

Blocking queues are very useful. In back-end development, based on blocking queues, the producer-consumer model can be implemented

The producer-consumer model is a way to deal with multi-threading problems. For example:

There are three steps to making dumplings: 1. Kneading the dough 2. Rolling out the dumpling wrappers 3. Making the dumplings

During the Chinese New Year, when the family makes dumplings together, there are two strategies:

1. Each person is responsible for rolling out the dumpling skins, and wrapping each one after rolling it out is relatively inefficient.

2. One person is responsible for rolling out dumpling wrappers, and the other two people are responsible for making dumplings [producer-consumer model]

Producer: The person who rolls out the dumpling wrappers Consumer: The person who makes the dumplings Trading place: The place where the dumpling wrappers are placed

Advantages of the producer-consumer model :

1. Decoupling

Decoupling means “reducing the coupling between modules”

Consider a distributed system:

When there are only A and B in the computer room, A directly sends the request to B, and the coupling between A and B is more obvious.

(1) If B dies, it may have a great impact on B. If A dies, it will also have a great impact on B.

(2) Assuming that a server C is added at this time, it is necessary to make major changes to the code of A.

At this time, if the producer-consumer model is introduced and a blocking queue is introduced, the above problems can be effectively solved.

At this time, A and B are well decoupled through the blocking queue.

At this time, if A or B hangs up, it will not have much impact since they do not interact directly with each other.

If you want to add a new server C, server A does not need any modification at this time, just let C also take elements from the queue.

2. Peak shaving and valley filling

The requests that the server receives from clients/users are not static. The number of requests may increase sharply due to some emergencies.

There is an upper limit on the number of requests that a server can handle at the same time, and different servers have different upper limits.

In a distributed system, it often happens that some can bear greater pressure, while others can bear less pressure.

At this time, every time A receives a request, B needs to process a request immediately

If A can withstand greater pressure and B can withstand less pressure, B may die first.

But if you use the producer consumption model, it’s a different story

When external requests suddenly surge and A receives more requests, A will write more request data into the queue, but B can still process the requests according to the original rhythm and will not hang up.

It is equivalent to the queue acting as a buffer. The queue takes up the pressure that was originally on B (peak cutting)

The peak is often only temporary. When the peak subsides, A receives fewer requests, and B still handles them according to the established rhythm, and B will not be too idle (filling the valley)

Because the producer-consumer model is so important, although the blocking queue is just a data structure, we will implement this data structure separately into a server program and deploy it using a separate host/host cluster. At this time, this so-called blocking queue, It evolved into a "message queue"

The Java standard library already provides a ready-made implementation of blocking queues

 Array This version is faster, but only if you know the maximum number of elements.

If you don’t know how many elements there are, it is more appropriate to use Linked (for Array, frequent expansion is a big overhead)

For BlockingQueue, offer and poll do not have blocking functions, while put and take do.

Write a simple producer-consumer model based on blocking queue:

One thread produces and one thread consumes

//生产者消费者模型

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo19 {
    public static void main(String[] args) {
        //搞一个阻塞队列,作为交易场所
        BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();

        //负责生产元素
        Thread t1 = new Thread(() ->{
            int count = 0;
            while(true){
                try {
                    queue.put(count);
                    System.out.println("生产元素: " + count);
                    count++;

                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //负责消费元素
        Thread t2 = new Thread(() ->{
            while(true){
                try {
                    Integer n = queue.take();
                    System.out.println("消费元素: " + n);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

Implement a blocking queue yourself:

Here, the blocking queue is implemented based on arrays and circular queues.

We connect the arrays end to end to form a ring: when head and tail overlap, are they empty or full?

(1) A grid is wasted. When the tail reaches the previous position of the head, it is considered full.

(2) Create a separate variable and use a separate variable to represent the current number of elements.

Now, we have implemented a simple queue:

class MyBlockingQueue{
    //使用一个 String 类型的数组来保存元素,假设这里只存 String
    private String[] items = new String[1000];
    //指向队列的头部
    private int head = 0;
    //指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
    //当 head 和 tail 相等(重合),相当于空的队列
    private int tail = 0;
    //使用 size 来表示元素个数
    private int size = 0;

    //入队列
    public void put(String elem){
        if (size >= items.length){
            //队列满了
            return;
        }
        items[tail] = elem;
        tail++;
        if (tail >= items.length){
            tail = 0;
        }
        size++;
    }
    //出队列
    public String take(){
        if (size == 0){
            //队列为空,暂时不能出队列
            return null;
        }
        String elem = items[head];
        head++;
        if (head >= items.length){
            head = 0;
        }
        size--;
        return elem;
    }
}

Next, we transform the above ordinary queue into a blocking queue

(1) Thread safety

First lock put and take to ensure thread safety when called by multiple threads.

So we directly add synchronized to put and take. 

In addition to locking, memory visibility issues must also be considered

At this time, we add volatile to head, tail, and size.

(2) Implement blocking

(1) When the queue is full, another put will cause blocking.

(2) When the queue is empty, taking again will cause blocking.

The two waits here will not appear at the same time. Either wait here or wait on the other side. 

In the above code, if the condition is met, wait is performed. When wait is awakened, will the condition be broken?

For example, currently, my put operation is blocked because the queue is full. After a while, wait is awakened. When it is awakened, will the queue be full at this time? Is it possible that the queue is still full?

In case of wake-up, the queue is still full, which means that the subsequent code continues to execute, and the previously stored elements may be overwritten.

In the current code, if it is awakened by interrupt, an exception will be caused directly at this time, and the method will end and will not continue to execute, which will not cause the problem of overwriting existing elements just mentioned.

But if it is written according to the try-catch method, once the interrupt wakes up at this time, the code will go down and enter the catch. After the catch is executed, the code will not end, but will continue to execute, which will trigger the " Override element" logic

The above analysis found that this code was just written correctly by luck. If it is slightly changed, problems will occur. When actually writing it like this, it is difficult to notice this detail. Is there a way to make this code foolproof, even if it is written in a try-catch manner? , is everything okay?

Just let wait wake up and then judge the condition again

If the condition is still that the queue is full, continue to wait. If the condition is that the queue is not full, you can continue execution.

The purpose of while here is not to loop, but to use loops to cleverly implement wait. After waking up, confirm the conditions again. 

Therefore, when using wait, it is recommended to use while for conditional determination.

The final code is as follows: 

class MyBlockingQueue{
    //使用一个 String 类型的数组来保存元素,假设这里只存 String
    private String[] items = new String[1000];
    //指向队列的头部
    volatile private int head = 0;
    //指向队列的尾部的下一个元素,总的来说,队列中的有效元素范围:[head,tail)
    //当 head 和 tail 相等(重合),相当于空的队列
    volatile private int tail = 0;
    //使用 size 来表示元素个数
    volatile private int size = 0;

    //入队列
    public synchronized void put(String elem) throws InterruptedException {
        while (size >= items.length){
            //队列满了,
            this.wait();
        }
        items[tail] = elem;
        tail++;
        if (tail >= items.length){
            tail = 0;
        }
        size++;
        //用来唤醒队列为 空 的阻塞情况
        this.notify();
    }
    //出队列
    public synchronized String take() throws InterruptedException {
        while (size == 0){
            //队列为空,暂时不能出队列
            this.wait();
        }
        String elem = items[head];
        head++;
        if (head >= items.length){
            head = 0;
        }
        size--;
        //使用这个 notify 来唤醒队列满的阻塞情况
        this.notify();
        return elem;
    }
}

3. Timer

Timers are also common components in daily development, similar to an alarm clock

TimerTask is similar to the Runnable we learned before. It will record a time: when the current task will be executed.

The task registered in the timer is not executed in the thread that calls the schedule, but is executed by the thread inside the Timer.

import java.util.Timer;
import java.util.TimerTask;

public class demo21 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //给 timer 中注册的这个任务,不是在调用 schedule 的线程中执行的,而是通过 Timer 内部的线程来负责执行的
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行");
            }
        },3000);
        System.out.println("程序开始运行");
    }
}

We will find that after running the results, the code does not end the process after printing:

 This is because:

Timer has its own thread internally. In order to ensure that newly scheduled tasks can be processed at any time, this thread will continue to execute, and this thread is also a foreground thread. 

Next, try implementing a timer yourself:

A timer can have many tasks!

First, you must be able to describe a task, and then use a data structure to organize multiple tasks.

(1) Create a class like TimerTask to represent a task. This task needs to include two aspects: the content of the task and the actual execution time of the task.

The actual execution time of the task can be represented by a timestamp. When scheduling, first obtain the current system time, and then based on this, add the delay time interval to get the actual time to execute the task.

(2) Use a certain data structure to organize multiple TimerTasks

If you use List (array, linked list) to organize Timertask, if there are too many tasks, how to determine which task can be executed when?

In this way, you need to create a thread to continuously traverse the above List to see if each element here has reached the time. When the time is up, it will be executed. If the time is not up, it will be skipped.

But this idea is not scientific. If the time for these tasks is still too early, the scanning thread here will need to scan repeatedly before the time arrives. This process is very inefficient.

(a) It is not necessary to scan all tasks, just focus on the tasks with the earliest time.

If the earliest task time has not arrived yet, other task times will not arrive even more.

Improved traversing all tasks to focus on only one

So how do you know which task is the top priority?

It is most appropriate to use a priority queue to organize all tasks. The first element of the team is the task with the smallest time.

(b) Scanning for this task does not need to be performed repeatedly.

Instead, after obtaining the time of the first element of the queue, a difference is made with the current system time, and the sleep/waiting time is determined based on this difference. Before this time is reached, repeated scanning will not be performed, which greatly reduces number of scans

"Improving efficiency" here does not mean shortening execution time, but reducing resource utilization and avoiding unnecessary CPU waste.

(c) Create a scanning thread that is responsible for monitoring whether the task of the head element of the team has expired.

The core process of this code is as follows:

import java.util.PriorityQueue;

//创建一个类,用来描述定时器中的一个任务
class MyTimerTask{
    //任务啥时候执行,毫秒级的时间戳
    private long time;
    //任务具体是啥
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable ruunnabl, long delay){
        //delay 是一个相对的时间差
        //构造 time 要根据当前系统时间和 deley 进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = ruunnabl;
    }
}

//定时器类的本体
class MyTimer{
    //使用优先级队列,来保存上述的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,就是把要执行的任务给添加到队列中
    public void schedule(Runnable runnable,long delay){
        MyTimerTask task = new MyTimerTask(runnable,delay);
        queue.offer(task);
    }

    //MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() ->{
            while(true){
                if (queue.isEmpty()){
                    //注意,当前队列为空,此时就不应该去取这里的元素
                    continue;
                }
                MyTimerTask task = queue.peek();
                long curTime = System.currentTimeMillis();
                if (curTime >= task.getTime()){
                    //需要执行任务
                    queue.poll();
                    task.getRunnable().run();
                }else {
                    //让当前扫描线程休眠一下,就可以按照时间差进行休眠
                    try {
                        Thread.sleep(task.getTime() - curTime);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
    }
}

In the above code, the core logic of the timer has been written, but there are still several key issues in this code:

1. Not thread safe

 This collection class is not thread-safe and will be used in both the main thread and the scanning thread.

Lock operations on the queue

2. In the scanning thread, is it appropriate to directly use sleep to sleep?

inappropriate

(a) After sleep enters blocking, the lock will not be released, affecting other threads and the schedule executed here.

(b) During sleep, it is inconvenient to interrupt early (although you can use interrupt to interrupt, but interrupt means that the thread should end)

Assume that the current top task is executed at 14:00, and the current time is 13:00 (sleep for one hour). If we add a new task at this time, the new task will be executed at 13:30. At this time, the new task will be executed at 13:30. The task becomes the earliest task to be executed.

We hope that every time a new task comes, we can wake up the previous dormant state and make a new judgment based on the latest task status.

At this time, the scanning thread can block and wait for 30 minutes.

In contrast, wait is more appropriate : wait can also specify a timeout, and wait can also be awakened in advance.

3. If you write any class, can its objects be placed in the priority queue?

Requires elements placed into the priority queue to be made "comparable"

Define comparison rules between tasks through Comparable or Comparator

Here, right here in MyTimerTask, Comparable is implemented

The final code is as follows:

import java.util.PriorityQueue;

//创建一个类,用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //任务啥时候执行,毫秒级的时间戳
    private long time;

    @Override
    public int compareTo(MyTimerTask o) {
        //把时间小的,优先级高,最终时间最小的元素,就会放到队首
        return (int) (this.time - o.time);
    }

    //任务具体是啥
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public MyTimerTask(Runnable ruunnabl, long delay){
        //delay 是一个相对的时间差
        //构造 time 要根据当前系统时间和 deley 进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = ruunnabl;
    }
}

//定时器类的本体
class MyTimer{
    //用来加锁的对象
    private Object locker = new Object();

    //使用优先级队列,来保存上述的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,就是把要执行的任务给添加到队列中
    public void schedule(Runnable runnable,long delay){
        synchronized (locker)
        {
            MyTimerTask task = new MyTimerTask(runnable,delay);
            queue.offer(task);
            //每次来新的任务,都唤醒一下之前的扫描线程,好让扫描线程根据最新的额任务情况重新规划等待时间
            locker.notify();
        }
    }

    //MyTimer 中,还需要构造一个 “扫描线程” ,一方面去负责监控队首元素是否到点了们是否应该执行;一方面当任务到店之后
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() ->{
            while(true){
                synchronized (locker){
                    while (queue.isEmpty()){
                        //注意,当前队列为空,此时就不应该去取这里的元素
                        //此处使用 wait 等待更合适,如果使用 continue ,就会使这个线程的 while 循环运行的飞快
                        //也会陷入一个高频占用 cpu 的状态(忙等)
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask task = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if (curTime >= task.getTime()){
                        //需要执行任务
                        queue.poll();
                        task.getRunnable().run();
                    }else {
                        try {
                            //让当前扫描线程休眠一下,就可以按照时间差进行休眠
                            locker.wait(task.getTime() - curTime);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        t.start();
    }
}

4. Thread pool

Pool makes a very important method

If we need to frequently create and destroy threads, the cost of creating and destroying threads at this time cannot be ignored.

Therefore, you can use the thread pool to create a wave of threads in advance. If you need to use the thread later, just take one from the pool. When the thread is no longer used, put it back into the pool.

Originally, threads needed to be created/destroyed, but now ready-made threads are obtained from the pool and returned to the pool.

Why is fetching something from the pool faster and more efficient than creating threads from the system?

If you create a thread from the system, you need to call the system API, and then the operating system kernel completes the thread creation process.

The kernel provides services to all processes , so this is uncontrollable. 

If the thread is obtained from the thread pool, the above-mentioned operations in the kernel have been done in advance. The current process of obtaining the thread is completed by pure user code (pure user mode) and controllable .

The Java standard library also provides a ready-made thread pool

 Factory pattern: producing objects

Generally, objects are created through new and through the constructor method.

However, the constructor method has a major flaw: the name of the constructor method is fixed to the class name.

Some classes require multiple different construction methods, but the name of the construction method is fixed and can only be implemented by method overloading (the number and type of parameters need to be different)

For example: Now we want to construct in two ways, one is to construct according to Cartesian product coordinates, and the other is to construct according to polar coordinates. The number and type of parameters in these two construction methods are the same, and they cannot constitute overloading. , an error will be reported during compilation

At this point, you can use factory mode to solve the above problems.

Use the factory pattern to solve the above problems: instead of using constructors, use ordinary methods to construct objects. The name of such a method can be arbitrary. Inside the ordinary method, create a new object.

Since the purpose of ordinary methods is to create objects, such methods are generally static.

After the thread pool object is ready, you can add tasks to the thread pool using the submit method.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//线程池
public class Demo23 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        for (int i = 0;i < 100;i++){
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

In addition to the above thread pools, the standard library also provides a thread pool with a richer interface: ThreadPoolExecutor

The previous thread pools have been encapsulated for ease of use. ThreadPoolExecutor has many options for us to adjust, which can better meet our needs.

Let’s talk about the parameters and meaning of the thread pool construction method in the Java standard library

If the thread pool is compared to a company, the number of core threads is the number of formal employees.

The maximum number of threads is the number of regular employees + interns

When the company's business is not busy, it does not need interns. When the company's business is busy, it can find some interns to share the tasks.

It can be done, not only to ensure efficient processing of tasks when busy, but also to ensure that resources are not wasted when idle.


Intern threads can be destroyed if their idleness exceeds a certain threshold.


There are many tasks inside the thread pool. These tasks can be managed using blocking queues. 

The thread pool can have a built-in blocking queue, and you can manually specify one


Factory pattern, create threads through this factory class 


This is the focus of thread pool inspection: rejection method/rejection strategy

The thread pool has a blocking queue. When the blocking queue is full, tasks continue to be added. How to deal with this?


First rejection strategy:

Throw an exception directly and the thread pool will stop working. 


Second rejection strategy:

Whoever is the thread that adds this new task will execute this task


Third rejection strategy:

Discard the oldest task and execute the new task


The fourth rejection strategy:

Directly discard the new task and continue with the previous task

There are two groups of thread pools mentioned above: one group of thread pools are encapsulated Executors, and one group of thread pools are native ThreadPoolExecutors. You can use either one, mainly depending on the actual needs.

Next, we try to simulate and implement a thread pool ourselves:


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();

    //通过这个方法,来把任务添加到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // n 表示线程池里面有几个线程
    //创建了一个有固定数量的线程池
    public MyThreadPool(int n){
        for(int i = 0;i < n;i++){
            Thread t = new Thread(() ->{
                while(true){
                    //循环的取出任务,并执行
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
}

//线程池
public class Demo24 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(4);
        for(int i = 0;i < 1000;i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //要执行的工作
                    System.out.println(Thread.currentThread().getName() + " hello");
                }
            });
        }
    }
}

Since the scheduling at this time is random, when the tasks currently inserted into this thread pool are executed, the workload of the N threads may not be completely equal, but in a statistical sense, the task loads are equal.

In addition, there is another important question:

When creating a thread pool, how does the number of threads come from?

In different projects, the work done by threads is different.

The work of some threads is "CPU-intensive", and the work of threads is all calculations

Most of the work has to be done on the CPU. The CPU has to arrange cores for it to complete the work before it can make progress. If the CPU has N cores and the number of threads is also N, the ideal situation is to have one on each core. thread

If there are a lot of threads, the threads are waiting in the queue and there will be no new progress.

The work of some threads is "IO intensive", including reading and writing at night, waiting for user input, and network communication.

It involves a lot of waiting time. During the waiting process, the CPU is not used. Even if there are more threads, it will not put too much burden on the CPU.

In actual development, part of a thread's work is often CPU-intensive and part of its work is IO-intensive.

At this time, it is uncertain how many threads are running on the CPU and how many are waiting for IO.

A better approach here is to find the appropriate number of threads through experiments, try different numbers of threads through performance testing, and during the trial process, find a value that is more balanced between performance and system resource overhead.


6. Common lock strategies

The lock strategy explained next is not limited to Java. Any topic related to " lock " may involve the following content . This
These features are mainly for reference by lock implementers .
Lock strategy does not refer to a specific lock. It is an abstract concept that describes the characteristics of a lock.

1. Optimistic locking VS pessimistic locking

Optimistic locking: Predict that lock conflicts are unlikely to occur in this scenario

Pessimistic lock: predicting this scenario, it is very easy to have a lock conflict

Lock conflict: Two threads try to acquire a lock, one thread can acquire it successfully, and the other thread blocks and waits.

Whether the probability of lock conflict is high or low will have a certain impact on subsequent work.


2. Heavyweight lock VS lightweight lock

Heavyweight lock: The cost of locking is relatively large (it takes more time and takes up less system resources) 

Lightweight lock: The cost of locking is relatively small (it takes less time and takes up less system resources)

A pessimistic lock is likely to be a heavyweight lock (not absolute)

An optimistic lock, also probably a lightweight lock (not absolute)

Pessimism and optimism are based on the prediction of the probability of lock conflicts before locking, which determines the amount of work; light weight is based on considering the actual lock overhead after locking.

Officially, because such concepts overlap, for a specific lock, it may be called an optimistic lock, or it may be called a lightweight lock.


3. Spin lock VS suspend waiting lock

Spin lock: It is a typical implementation of lightweight lock

It often means that in user mode, through self-selection (while loop), a lock-like effect is achieved.

It will consume a certain amount of CPU resources, but it can get the lock as quickly as possible. 

Suspended waiting lock: It is a typical implementation of heavyweight lock

Through the kernel state and the lock mechanism provided by the system, when a lock conflict occurs, it will involve the kernel's scheduling of threads, causing the conflicting thread to hang (block and wait).


4. Read-write lock VS mutex lock

Read-write lock: separates the locking of read operations and the locking of write operations

Multiple threads reading the same variable at the same time does not involve thread safety issues.

If there are two threads, one thread reads and locks , and the other thread also reads and locks , there will be no lock competition.

If there are two threads, one thread is write-locked , and the other thread is also write-locked , lock competition will occur.

If there are two threads, one thread is locked for writing and the other thread is also locked for reading , lock competition will occur.

In actual development, the frequency of read operations is often much higher than that of write operations. 

The Java standard library also provides ready-made read-write locks


5. Fair lock VS unfair lock

As defined here, a fair lock is a first-come, first-served lock.

Unfair locks appear to have equal probability, but are actually unfair (the blocking time of each thread is different)

The operating system's own lock (pthread_mutex) is an unfair lock.

In order to implement fair locks, some additional data structures are needed to support it (for example, there needs to be a way to record the blocking waiting time of each thread)


6. Reentrant lock VS non-reentrant lock

If a thread locks a lock twice in a row, a deadlock will occur, which is a non-reentrant lock.

If the code is written like this, will there be any problems?

1. Call the method and first lock this. Assume that the lock is successful at this time.

2. Next, execute down to synchronized in the code block. At this time, this is still locked.

At this time, lock competition will occur because the this object is already in a locked state.

At this time, the thread will block until the lock is released before it has a chance to get the lock.

In this code, the lock on this can only be released after the increase method is executed. However, the lock must be successfully acquired the second time before the method can continue to execute.

If you want the code to continue to execute downwards, you need to obtain the second lock, that is, release the first lock. If you want to release the first lock, you need to ensure that the code continues to execute.

At this time, because the lock of this cannot be released, the code is stuck here, and the thread is frozen (the first manifestation of deadlock)

If it is a non-reentrant lock, the lock will not save which thread locked it. As long as it receives a "lock" request, it will reject the current lock, regardless of which thread the current thread is. , a deadlock will occur

A reentrant lock will save which thread added the lock. After receiving a subsequent lock request, it will first compare to see if the locked thread is the thread currently holding the lock. At this time You can decide flexibly

synchronized itself is a reentrant lock, so this code will not deadlock.

Reentrant locks allow the lock to record which thread currently holds the lock.

If the locking is N levels, when encountering }, how does the JVM know whether the current } is the outermost one?

Just let the lock hold a counter. Let the lock object not only record which thread holds the lock, but also record how many times the current thread has added the lock through an integer variable.

Every time a locking operation is encountered, the counter is +1, and every time an unlocking operation is encountered, the counter is -1.

When the counter is reduced to 0, the lock release operation is actually performed, and it is not released at other times.

We call this counting "reference counting"


7. Deadlock

1. One thread, one lock, but it is a non-repeatable lock. If the thread locks the lock twice in a row, a deadlock will occur.

2. Two threads, two locks. The two threads first acquire a lock respectively, and then try to acquire the other party’s lock at the same time.

3. N threads, N locks

Feel the deadlock through the code;

public class Demo25 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2){
                    System.out.println("t1 两把锁加锁成功");
                }
            }
        });
        
        Thread t2 = new Thread(() ->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1){
                    System.out.println("t2 两把锁加锁成功");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

This code is a typical second type of deadlock. After running it, we will find that neither thread can successfully print out the result.

 We can open jconsole to further observe the thread

We will find that both threads are stuck in further locking places

If it is a server program and a deadlock occurs, the deadlocked thread will freeze and cannot continue to work, which will have a serious impact on the program.

3. N threads, N locks

Dining philosophers problem:

Suppose there are five philosophers and 5 chopsticks on the table 

Every philosopher mainly has to do two things:

1. Think about life and put down your chopsticks

2. When eating noodles, you can pick up the chopsticks in your left and right hands and pick up the noodles to eat.

 Other settings:

1. It is uncertain when every philosopher will think about life and when he will eat noodles.

2. Every philosopher, once he wants to eat noodles, will be very stubborn to complete the operation of eating noodles. If his chopsticks are used by others, he will be blocked and wait, and during the waiting process, he will not put down his hands. holding chopsticks

Based on the above model settings, these philosophers can all operate very well.

However, if an extreme situation occurs, a deadlock will occur

Suppose that at the same moment, five philosophers all want to eat noodles, and at the same time stretch out their left hands, pick up the chopsticks on the left, and then try to pick up the chopsticks on the right.

5 philosophers are 5 threads, 5 chopsticks are 5 locks

The philosopher model vividly describes the third type of deadlock problem.

So is there a way to avoid deadlock?

First, let’s clarify the reasons for deadlock and the necessary conditions for deadlock.

Four necessary conditions: (one is indispensable)

As long as any one of the conditions can be violated, deadlock can be avoided.

1. Mutual exclusion is used. After one thread acquires a lock, other threads cannot acquire the lock.

The locks actually used are generally mutually exclusive (the basic characteristics of locks)

2. It cannot be preempted. The lock can only be actively released by the holder and cannot be snatched away by other threads.

It is also the basic characteristic of locks

3. Request and hold. A thread tries to acquire multiple locks. During the process of trying to acquire the second lock, it will maintain the acquisition status of the first lock.

Depends on code structure (most likely affects requirements)

4. Waiting in a loop, t1 tries to get locker2, needs t2 to finish executing, releases locker2, t2 tries to get locker1, needs t1 to finish executing, releases locker1

Depends on code structure (key points to solve deadlock problem)

How to specifically solve the deadlock problem? There are many practical methods (Banker’s Algorithm)

The banker's algorithm can solve the deadlock problem, but it is not very practical.

Here, we introduce a simpler and more effective way to solve the deadlock problem

Number the locks and specify the order in which they should be locked

For example: it is stipulated that if each thread wants to acquire multiple locks, it must first acquire the lock with the smaller number, and then acquire the lock with the larger number.

As long as the order of locking of all threads strictly follows the above order, there will be no waiting for locking.


8. Synchronized principle

1. Basic features 

What specific strategies does synchronized use?

1. Synchronized is both a pessimistic lock and an optimistic lock.

2. Synchronized Even heavyweight locks are lightweight locks

3. The synchronized heavyweight lock part is implemented based on the system's mutex lock, and the lightweight lock part is implemented based on the spin lock.

4. Synchronized is an unfair lock (it does not follow the first-come, first-served principle. After the lock is released, which thread gets the lock depends on its ability)

5. Synchronized is a reentrant lock (it will internally record which thread has obtained the lock and record the reference count)

6. Synchronized is not a read-write lock


2. Locking process 

The internal implementation strategy (internal principle) of synchronized

After a synchronized is written in the code, a series of "adaptive processes" and lock upgrades (lock expansion) may occur here.

No lock->biased lock->lightweight lock->heavyweight lock

Biased locking is not a real lock, but just a "mark". If other threads compete for the lock, the lock will be really locked. If there are no other threads competing, the lock will not be really locked from beginning to end. locked

The locking itself has a certain cost. If you can't add it, don't add it. Someone will have to compete to actually add the lock.

Lightweight locks, synchronized implement lightweight locks through spin locks

If the lock is occupied here, another thread will repeatedly query whether the current lock status has been released according to the spin method.

However, if more and more threads compete for this lock in the future (lock conflicts become more intense), synchronized will be upgraded from a lightweight lock to a heavyweight lock.

Lock elimination, the compiler will intelligently determine whether the current code needs to be locked. If you write the lock first, but there is actually no need to lock, the locking operation will be automatically deleted.

The compiler performs optimization to ensure that the logic after optimization is consistent with the previous logic.

This will make some optimizations become conservative

We programmers cannot rely solely on optimization to improve code efficiency. We ourselves must also play some role. Determining when to lock is also a very important task

lock roughening

 "Lock granularity": If the locking operation contains more code that is actually executed, the lock granularity is considered to be larger.

Sometimes, we hope that the granularity of the lock is smaller and the degree of concurrency is higher.

Sometimes, it is better to have a larger lock granularity, because locking and unlocking itself also have overhead.


 9. CAS

1. Concept 

CAS: full name Compare and swap , literal meaning : " Compare and swap " ,
Able to compare and exchange the value in a register with the value in memory to see if they are equal . If they are equal, exchange the value in another register with memory.
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

 This is a piece of pseudo code

Based on CAS, a set of "lock-free" programming can be derived

The scope of use of CAS has certain limitations.


2. Application of CAS

(1) Implement the atomic class

For example, multiple threads perform ++ on a count variable

In the java standard library, a set of atomic classes have been provided

Within the atomic class, no locking operations are performed, only thread-safe auto-increment is completed through CAS.

import java.util.concurrent.atomic.AtomicInteger;

//使用原子类
public class Demo26 {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
           for(int i = 0;i < 50000;i++){
               //相当于 count++
               count.getAndIncrement();
               //相当于 ++count
               //count.incrementAndGet();
               //count--
               //count.getAndDecrement();
               //--count
               //count.decrementAndGet();
           }
        });

        Thread t2 = new Thread(() ->{
           for (int i = 0;i < 50000;i++){
               count.getAndIncrement();
           }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}

The above atomic class is implemented based on CAS

Pseudocode implementation:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

When two threads execute ++ concurrently, without any restrictions, it means that the two ++ are serial and can be calculated correctly. Sometimes, the two ++ operations are interspersed. , problems may arise at this time

Locking ensures thread memory security. Through locking, interleaving is forcibly avoided. 

Atomic class/CAS ensures thread safety: Use CAS to identify whether there is currently "interleaving". If there is no interleaving, modify it directly at this time, which is safe; if interleaving occurs, re-read the latest value in the memory and repeat it again. try to modify

CAS is an instruction, and the instruction itself cannot be split.

CAS itself is a single instruction, which actually includes the operation of accessing memory. When multiple CPUs try to access memory, there will essentially be a sequence.

Why do these two CAS access memory sequentially?

Multiple CPUs operating the same resource will also involve lock competition (instruction-level locks). This lock is much lighter than the synchronized code-level locks we usually call


(2) Implement spin lock

Implement more flexible locks based on CAS and gain more control .
Spin lock pseudo code:
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}


 3. CAS’s ABA issues

The key point of CAS is to compare the value of register 1 and the memory, and determine whether the value of the memory has changed by whether they are equal.

If the memory value changes, other threads have modified it.

If the memory value has not changed and no other thread has modified it, subsequent modifications are safe.

But there is a problem here: if the value here has not changed, does it mean that no other thread has modified it?

A - B - A Problem: Another thread changes the variable from A -> B, and then from B -> A. At this time, the current thread cannot distinguish whether the value has never changed, or whether it has changed and then changed back.

In most cases, even if there is an ABA problem, it will not have any impact, but if you encounter some extreme scenarios, it may not be the case.

Suppose there is 100 yuan in the account now and you want to withdraw 50 yuan

Suppose an extreme situation occurs: when you press the first button to withdraw money, it gets stuck, so you press it again (two withdrawal requests are generated, and the ATM uses two threads to process these two requests)

Assuming that withdrawals are made according to the CAS method, each thread operates as follows:

1. Read the account balance and put it in the variable M

2. Use CAS to determine whether the current actual amount is still M. If so, modify the actual balance to M - 50. If not, abandon the current operation (operation fails)

When only two threads t1 and t2 perform the withdrawal operation of 50, due to CAS, the money will only be successfully deducted once. 

But suppose that another person transfers 50 yuan to the account at this time, so that after the t2 operation is completed, the balance changes back to the initial 100 yuan, causing t1 to mistakenly think that no other thread has modified the balance, so it continues The deduction operation was performed, resulting in repeated deductions. At this time, the A-B-A problem occurred.

Although the probability of the above operations is relatively small, it still needs to be considered

Regarding the ABA problem, the basic idea of ​​CAS is OK, but the main reason is that the modification operation can jump repeatedly, which can easily invalidate the judgment of CAS.

CAS determines "the values ​​are the same", but actually expects "the values ​​have not changed"

If it is agreed that the value can only change in one direction (for example, it can only increase in one direction and cannot decrease), the problem will be easily solved.

But it seems that the account balance cannot only increase but not decrease.

Although the balance doesn't work, the version number does! ! !

At this time, to measure whether the balance has changed, we don’t just look at the value of the balance, but at the version number!

Arrange a next-door neighbor for the account balance: version number (value increases, does not decrease)

Use CAS to determine whether the version numbers are the same. If the version numbers are the same, the data must not have been modified. If the data has been modified, the version number must be increased.


10. Common classes of JUC (java.util.concurrent)

concurrent: concurrency (multi-threading)

1、Collable interface

It is also a way to create threads

Runnable can represent a task (run method) and returns void

Callable can also represent a task (call method) and return a specific value. The type can be specified through generic parameters.

If you are performing multi-threaded operations, if you only care about the process of multi-threaded execution, use Runnable.

Like thread pools and timers, they are only related to processes and use Runnable

If you are concerned about multi-threaded calculation results, it is more appropriate to use Callable

To calculate a formula through multi-threading, it is more appropriate to use Callable

Code example: Create a thread to calculate 1 + 2 + 3 + ... + 1000, without using the Callable version
static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}
Code example: Create a thread to calculate 1 + 2 + 3 + ... + 1000, using the Callable version
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo27 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1;i <= 1000;i++){
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        
        Integer result = futureTask.get();
        System.out.println(result);
    }
}

When using Callable, you cannot directly use Thread's constructor parameters.

You can use an auxiliary class: FutureTask 

 So when can this result be calculated? This problem can be solved by using FutureTask:

Similar to when you go to eat Malatang, after paying the money, the boss will give you a receipt. When the Malatang is ready, you can use the receipt to pick up the meal. Here, you use FutureTask to get the result.


2、ReentrantLock

Reentrant mutex lock . Similar to synchronized positioning , it is used to achieve mutual exclusion effects and ensure thread safety.
ReentrantLock is also a reentrant lock. The original meaning of the word "Reentrant" is "reentrant"
This lock is not as commonly used as synchronized, but it is also an optional locking component.
This lock is closer to the lock in C++ in use.

lock() lock unlock() unlock

ReentrantLock has some features that synchronized does not have:

1. Provides a tryLock method for locking

For the lock operation, if the lock is unsuccessful, it will block and wait (wait to death) 

For trylock, if the lock fails, false is returned directly / you can also set the waiting time

tryLock provides more operational space for locking operations

2. ReentrantLock has two modes, which can work in fair lock state or unfair lock state.

Fair/unfair mode that can be set via parameters in the constructor

3. ReentrantLock also has a waiting notification mechanism, which can be completed with a class like Condition.

The wait notification here is more powerful than wait notify.

These are the advantages of ReentrantLock

But the disadvantage of ReentranLock is also obvious: unlock is easy to miss; therefore we can use finally to execute unlock

The synchronized lock object is any object, and the ReentrantLock lock object is itself.

In other words, if multiple threads call the lock method for different ReentrantLocks, there will be no lock competition.

In actual development, when performing multi-thread development and using locks, synchronized is still the first choice. 


3. Atomic class

Most of the content has been discussed before

What are the application scenarios of atomic classes?

(1) Counting requirements

Number of plays, likes, coins, forwards, collections...

The same video has many people playing/liking/favoriting it at the same time...

(2) Statistical effect

Count the number of requests with errors, using the atomic class

Record the number of erroneous requests, write another monitoring server, obtain these error counts from the display server, and draw them on the page through a graph.

If after a program is released, the number of errors suddenly increases significantly, it means that there is a high probability that this version of the code contains bugs!


4. Semaphore _

Semaphores also often appear in operating systems

Semaphore is an important concept/component in concurrent programming

To be precise, Semaphore is a counter (variable) that describes the number of available resources and critical resources.

Critical resources: resources that can be commonly used by multiple processes/threads and other concurrently executing entities (multiple threads modify the same variable, this variable can be considered a critical resource)

It describes whether the current thread has critical resources available.

At the entrance of the parking lot, there is usually a display: there are XX vacant spaces in the parking lot, where this data is the semaphore, and the vacant parking spaces are the available resources.

If I drive into the parking lot, it is equivalent to applying for a parking space (applying for an available resource). At this time, the counter will be -1, which is called P operation, accquire (application)

If I drive out of the parking lot, it is equivalent to releasing a parking space (releasing an available resource). At this time, the counter will be +1, which is called V operation, release.

When the counter is 0, if you continue to perform the P operation, you will be blocked and wait until other threads perform the V operation and release an idle resource.

This blocking and waiting process is a bit like a lock

A lock is essentially a special semaphore (the value inside is either 0 or 1)

A semaphore is more general than a lock. It can describe not only one resource, but also N resources.

Although conceptually broader, there are still more locks in actual development (the binary semaphore scenario is more common)


5、CountDownLatch

A component for a specific scenario

For example: download something

Sometimes, downloading a relatively large file is relatively slow (not because of the speed limit at home, but often due to limitations on the server)

There are some multi-threaded downloaders that split a large file into several smaller parts and use multiple threads to download them.

Each thread is responsible for downloading a part, and each thread is a network connection.

Can greatly increase download speed

When a task needs to be divided into multiple tasks to complete, how to measure whether multiple tasks have been completed now?

At this time, you can use CountDownLatch 

import java.util.concurrent.CountDownLatch;

//CountDownLatch
public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法中,指定要创建几个任务
        CountDownLatch countDownLach = new CountDownLatch(10);
        for(int i = 0;i < 10;i ++){
            int id = i;
            Thread t = new Thread(() ->{
                System.out.println("线程" + id + " 开始工作");
                try {
                    //使用 sleep 代指某些耗时操作
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + id + " 结束工作");
                //在每个任务结束这里,调用一下方法
                //把 10 个线程想象成短跑比赛的 10 个运动员,countDown 就是运动员撞线了
                countDownLach.countDown();
            });
            t.start();
        }

        //主线程如何知道上述所有任务是否都完成了呢?
        //难道要在主线程中调用 10 次 join 吗?
        //万一要是这个任务结束,但是线程不需要结束,join 不也就不行了吗
        //主线程中可以使用 countDownLatch 负责等待任务结束
        // a -> all,等待所有任务结束,当调用 countDown 次数 < 初始设置的次数,await 就会阻塞
        countDownLach.await();
        System.out.println("多个线程的所有任务都执行完毕");
    }
}

11. Collection class

Collection classes, which ones are thread-safe?

Vector, Stack, HashTable are thread-safe (not recommended), but other collection classes are not thread-safe.
These three use synchronized in key methods, so they are considered thread-safe.
Vector and HashTable are both collection classes created in the early days of Java.
Adding a lock does not necessarily mean it is thread safe, and not adding a lock does not necessarily mean it is thread unsafe. To judge whether a code is thread safe, you must analyze the specific issues in detail.
Although the get and set methods of HashTable are synchronized, thread safety issues may occur if they are not used correctly.
(1) If there are multiple threads, executing set operations concurrently is thread-safe due to synchronized restrictions.
(2) If multiple threads perform some more complex operations, such as determining that the value of get is xxx and then performing set, such operations may not be thread-safe.
Therefore, although collection classes such as Vecdtor and Hashtable are synchronized, they cannot be guaranteed to be thread-safe. At the same time, in the case of a single thread, synchronized may affect the execution efficiency (lock elimination can only reduce the impact to a certain extent, but Not 100%)

(1) Using ArrayList in multiple environments

1. Use the synchronization mechanism yourself (synchronized or ReentrantLock)
A lot of related discussions have been done before. They will not be expanded upon here.
2、Collections.synchronizedList(new ArrayList);
 Collections.synchronizedList(new ArrayList);

ArrayList 本身没有使用 synchronized

但是你又不想自己加锁,就可以使用上面这个东西

能让 ArrayList 像 Vector 一样工作(很少使用)

3、使用 CopyOnWriteArrayList
写时复制
多线程同时修改同一个变量,是不安全的,那么如果多线程同时修改不同变量,是不是就安全了呢?

如果多线程取读取,本身就不会又任何线程安全问题

一旦有线程去修改,就会把自身复制一份 

尤其是修改如果比较耗时的话,其它线程还是从旧的数据上读取

一旦修改完成,就使用新的 ArrayList 去替换旧的 ArrayList (本质上就是一个引用的重新赋值,速度极快,并且又是原子的)

这个过程中,没有引入任何的加锁操作

使用了 创建副本 --> 修改副本 --> 使用副本替换 这样的流程来完成的线程安全的修改


(2)多线程环境使用队列

1) ArrayBlockingQueue 基于数组实现的阻塞队列
2) LinkedBlockingQueue 基于链表实现的阻塞队列
3) PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
4) TransferQueue 最多只包含一个元素的阻塞队列

(3)多线程环境使用哈希表

(a)Hashtable

HashMap 是线程不安全的

HashTable 是线程安全的,关键方法都提供了 synchronized

ConcurrentHashMap 是线程安全的 hash 表
HashTable adds synchronized directly to the method, which is equivalent to locking this

Any read operation on the ht object will involve locking this

At this time, if there are many threads that want to operate ht, fierce lock competition will definitely be triggered. In the end, these threads can only be queued one by one.
Join the team and execute in sequence (the degree of concurrency is very low)

 In the hash table, the key is calculated by the hash function, and finally the array subscript is obtained. When key1 and key2 finally get the number

When the group subscripts are the same, a hash conflict occurs.
The secondary detection method generally does not appear in the use of real hash tables, so we use the linked list method to solve this hash conflict.
As long as the length of the linked list is controlled and not too long, the time complexity is still O(1).
If the two modification operations are for two different linked lists, is there a thread safety issue?
Obviously not! !
If it happens that the current insertion operation triggers the expansion, it may have an impact. The expansion operation is an operation of considerable weight.
So the entire hash table was moved again. In comparison, the lock overhead was minimal.
Since there is no thread safety issue in simply modifying two linked lists, is there no need to add this lock?

The specific method is to arrange a lock for each linked list

There are many linked lists on a hash table, and the probability that two threads happen to be operating the same linked list at the same time is very low.

, the overall lock overhead is greatly reduced.

Since any synchronized object can be used for locking, you can simply use the head node of each linked list as the lock object.


(b)ConcurretnhashMap

Improvements to ConcurrentHashMap:

1. The granularity of locks is reduced. Each linked list has one lock. In most cases, lock conflicts will not be involved [Core]

2. CAS operations are widely used, and size++ will be performed at the same time. Operations like this will not cause lock conflicts.

3. The write operation is locked at the linked list level, but the read operation is not locked.

4. Optimized for expansion operations: progressive expansion

Once the expansion of HashTable is triggered, it will immediately complete the transfer of all elements in one go. This process is quite time-consuming.

Progressive expansion: Break it into parts. When expansion is needed, another larger array will be created, and then the data in the old array will be gradually moved to the new array. There will be a period of time when the old array The array and the new array exist at the same time

1. Add new elements and insert them into the new array

2. To delete elements, just delete the old array elements.

3. To find elements, both the new array and the old array must be searched.

4. Modify the element and put this element into the new array uniformly.

At the same time, each operation will trigger a certain degree of transfer. Each time you transfer a little bit, you can ensure that the overall time is not very long. After a little adds up, the transfer will gradually be completed, and the old array can be completely destroyed. Got it

Before Java8, ConcurrentHashMap used segmented locks. Starting from Java8, there is one lock for each linked list.

Segmented locks can improve efficiency, but they are not as good as one lock for each linked list, and the code is more complex to implement. 

Guess you like

Origin blog.csdn.net/weixin_73616913/article/details/132087954