Java JUC Concurrent Programming (Notes)


Information comes from: Detailed explanation of itbaima
concurrent programming source code

Let’s talk about multithreading again

JUC is more difficult to learn than the Java application layer. The recommended prerequisite knowledge at the beginning is: JavaSE multi-threading part (required) , operating system, JVM** (recommended)**, and computer composition principles. Mastering the prerequisite knowledge will make your learning easier. Among them, the JavaSE multi-threading part must be mastered, otherwise you will not be able to continue studying this tutorial! We will not repeat any knowledge from the JavaSE stage.

Dear friends, be sure to click the collection button (collection = learn)

Remember the multi-threading we learned in JavaSE? Let’s review:

On our operating system, many processes can run at the same time, and each process is isolated from each other and does not interfere with each other. Our CPU will allocate a time slice to each process through the time slice rotation algorithm, and switch to the next process to continue execution after the time slice is used up. In this way, multiple programs can run simultaneously at a macro level.

由于每个进程都有一个自己的内存空间,进程之间的通信就变得非常麻烦(比如要共享某些数据)而且执行不同进程会产生上下文切换,非常耗时,那么有没有一种更好地方案呢?

后来,线程横空出世,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),上下文切换速度也高于进程。

现在有这样一个问题:

public static void main(String[] args) {
    
    
    int[] arr = new int[]{
    
    3, 1, 5, 2, 4};
    //请将上面的数组按升序输出
}

按照正常思维,我们肯定是这样:

public static void main(String[] args) {
    
    
    int[] arr = new int[]{
    
    3, 1, 5, 2, 4};
		//直接排序吧
    Arrays.sort(arr);
    for (int i : arr) {
    
    
        System.out.println(i);
    }
}

而我们学习了多线程之后,可以换个思路来实现:

public static void main(String[] args) {
    
    
    int[] arr = new int[]{
    
    3, 1, 5, 2, 4};

    for (int i : arr) {
    
    
        new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep(i * 1000);   //越小的数休眠时间越短,优先被打印
                System.out.println(i);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    }
}

我们接触过的很多框架都在使用多线程,比如Tomcat服务器,所有用户的请求都是通过不同的线程来进行处理的,这样我们的网站才可以同时响应多个用户的请求,要是没有多线程,可想而知服务器的处理效率会有多低。

虽然多线程能够为我们解决很多问题,但是,如何才能正确地使用多线程,如何才能将多线程的资源合理使用,这都是我们需要关心的问题。

在Java 5的时候,新增了java.util.concurrent(JUC)包,其中包括大量用于多线程编程的工具类,目的是为了更好的支持高并发任务,让开发者进行多线程编程时减少竞争条件和死锁的问题!通过使用这些工具类,我们的程序会更加合理地使用多线程。而我们这一系列视频的主角,正是JUC

但是我们先不着急去看这些内容,第一章,我们先来补点基础知识。


并发与并行

我们经常听到并发编程,那么这个并发代表的是什么意思呢?而与之相似的并行又是什么意思?它们之间有什么区别?

比如现在一共有三个工作需要我们去完成。

Insert image description here

顺序执行

顺序执行其实很好理解,就是我们依次去将这些任务完成了:

Insert image description here

实际上就是我们同一时间只能处理一个任务,所以需要前一个任务完成之后,才能继续下一个任务,依次完成所有任务。

并发执行

并发执行也是我们同一时间只能处理一个任务,但是我们可以每个任务轮着做(时间片轮转):

Insert image description here

只要我们单次处理分配的时间足够的短,在宏观看来,就是三个任务在同时进行。

And our threads in Java are exactly this mechanism. When we need to process hundreds or thousands of tasks at the same time, it is obvious that the number of CPUs cannot keep up with the number of our threads, so at this time we are required to The program has good concurrency performance to handle a large number of tasks at the same time. Learning Java concurrent programming will allow us to know how to deal with high concurrency situations in actual future scenarios.

Parallel execution

Parallel execution breaks through the limitation that only one task can be processed at the same time. We can do multiple tasks at the same time:

Insert image description here

For example, if we want to perform some sorting operations, we can use parallel computing. We only need to wait for all subtasks to be completed, and finally summarize the results. Including the distributed computing model MapReduce, it also adopts parallel computing ideas.


Let’s talk about the lock mechanism again

When it comes to the lock mechanism, I believe you are familiar with it. In the JavaSE stage, we synchronizedimplement locks by using keywords, which can effectively solve the situation of resource competition between threads. So, synchronizedhow is the bottom layer implemented?

We know that use synchronizedmust be associated with a certain object. For example, if we want to lock a certain piece of code, then we need to provide an object as the lock itself:

public static void main(String[] args) {
    
    
    synchronized (Main.class) {
    
    
        //这里使用的是Main类的Class对象作为锁
    }
}

Let's take a look at what instructions will be used after it is turned into bytecode:

Insert image description here

The most critical one is monitorenterthe instruction. You can see that it is monitorexitmatched with it later (note that there are 2 here), monitorenterand monitorexitcorresponds to locking and releasing the lock respectively. monitorenterYou need to try to acquire the lock before execution. Each object has a monitormonitor . Correspondingly, here is to obtain the ownership of the object monitor. Once monitorthe ownership is held by a thread, other threads will not be able to obtain it (an implementation of the monitor model).

After the code execution is completed, we can see that there are two of them monitorexitwaiting for us, so why are there two here? Logically speaking, should n't monitorenterand monitorexitshould correspond one to one? Why do we need to release the lock twice here?

First, let's look at the first one. After the lock is released, a goto instruction will be entered immediately, jumping to line 15, and the instruction corresponding to our 15th line is the return instruction of the method. In fact, under normal circumstances, only the first A monitorexitrelease lock, after the lock is released, the content following the synchronization code block continues to execute downwards. The second one is actually used to handle exceptions. You can see that its location is on line 12. If an exception occurs during program operation, the second one will be executed, and exceptions will continue to be thrown through monitorexitinstructions athrow. Instead of jumping directly to line 15 and running normally.

Insert image description here

The lock actually synchronizedused is stored in the Java object header. We know that objects are stored in heap memory, and inside each object, there is a part of space used to store object header information, and in the object header information, Contains hashCodethe lock information used by Mark Word to store objects. In different states, the data structures it stores are somewhat different.

Insert image description here

Heavyweight lock

Before JDK6, synchronizedit had always been called a heavyweight lock, monitorrelying on the Lock implementation of the underlying operating system. Java threads were mapped to the native threads of the operating system, and the switching cost was high. After JDK6, the implementation of locks has been improved. Let’s start with the original heavyweight lock:

As we said, each object has a monitor associated with it. In the Java virtual machine (HotSpot), the monitor is implemented by ObjectMonitor:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

Each thread waiting for the lock will be encapsulated into an ObjectWaiter object and enter the following mechanism:

Insert image description here

ObjectWaiter will first enter the Entry Set and wait. When the thread obtains the object, it monitorenters the Owner area and sets the variables monitorin it ownerto the current thread. At the same time, monitorthe counter in countit increases by 1. If the thread calls the method, the wait()currently held variables will be released monitor. ownerRestores to nulldecremented countby 1, and at the same time, the thread enters the WaitSet collection and waits to be awakened. If the current thread completes execution, the value of the variable will be released monitorand reset so that other threads can access the object monitor.

Although this design idea is very reasonable, in most applications, the time that each thread occupies the synchronized code block is not very long. There is no need to suspend and then wake up the competing threads, and modern CPUs basically For multi-core operation, we can use a new idea to implement locks.

In JDK1.4.2, spin lock was introduced (enabled by default after JDK6). It will not suspend the thread in the waiting state, but continuously detect whether it can acquire the lock through an infinite loop. Since a single thread occupies the lock The time is very short, so the number of cycles will not be too many, and you may be able to get the lock and run it quickly. This is a spin lock. Of course, spin locks will perform very well only when the waiting time is very short, but if the waiting time is too long, since the loop requires the processor to continue to operate, this will only waste processor resources, so automatically The waiting time of the spin lock is limited, and it is 10 times by default. If it fails, the heavyweight lock mechanism will be used.

Insert image description here

After JDK6, the spin lock has been optimized. The limit of the number of spins is no longer fixed, but adaptive. For example, on the same lock object, the spin wait has just successfully obtained the lock, and holds If the thread of the lock is running, then the spin may be successful this time, so more spins will be allowed. Of course, if a lock often fails to spin, it is possible that the spin strategy will no longer be used, but a heavyweight lock will be used directly.

lightweight lock

Starting from JDK 1.6, lightweight locks were introduced in order to reduce the performance consumption caused by acquiring and releasing locks.

The goal of lightweight locks is to reduce the performance consumption caused by heavyweight locks in the absence of competition (not to replace heavyweight locks, in fact, it is a gamble that only one thread is occupying resources at the same time), including system calls caused by Switching between kernel mode and user mode, thread switching caused by thread blocking, etc. It is not like a heavyweight lock that requires a mutex from the operating system. How it works is as follows:

When the content in the synchronized code block is about to be executed, the Mark Word of the object will first be checked to see whether the lock object is occupied by other threads. If it is not occupied by any thread, a name will be created in the stack frame of the current thread. It is the space of Lock Record, used to copy and store the current Mark Word information of the object (officially called Displaced Mark Word).

Then, the virtual machine will use the CAS operation to update the object's Mark Word to the lightweight lock state (the data structure becomes a pointer to the Lock Record, which points to the current stack frame)

CAS (Compare And Swap) is a lock-free algorithm (we have explained it before in the Springboot stage). It does not lock the object. Instead, during execution, it checks whether the value of the current data is what we expected. If it is, then the replacement will be performed normally, if not, then the replacement will fail. For example, two threads both need to modify ithe value of a variable. The default value is 10. Now one thread wants to modify it to 20 and the other to 30. If they both use the CAS algorithm, access will not be locked, ibut Directly try to modify ithe value, but when modifying, you need to confirm iwhether it is 10. If it is, it means that other threads have not modified it. If not, it means that other threads have modified it. At this time, the modification task cannot be completed. Modification fail.

In the CPU, CAS operations use cmpxchginstructions, which can improve efficiency from the lowest hardware level.

If the CAS operation fails, it means that a thread may have entered the synchronization code block at this time. At this time, the virtual machine will check again whether the Mark Word of the object points to the stack frame of the current thread. If so, it means that it is not another thread, but The current thread already has the lock on this object, so you can just enter the synchronization code block with confidence. If not, it is indeed occupied by other threads.

At this time, the idea of ​​lightweight locks was wrong from the beginning (there are objects competing for resources at this time, and the bet has been lost), so the lock can only be expanded into a heavyweight lock, and the operation of the heavyweight lock is performed (note Lock expansion is irreversible)

Insert image description here

So, lightweight lock->failed->adaptive spin lock->failed->heavyweight lock

The unlocking process also uses the CAS algorithm. If the object's MarkWord still points to the thread's lock record, then use the CAS operation to exchange the object's MarkWord with the Displaced Mark Word copied to the stack frame. If the replacement fails, it means that other threads have tried to acquire the lock. When releasing the lock, the suspended thread needs to be awakened.

bias lock

Biased locks are more pure than lightweight locks. Simply eliminate the entire synchronization and no longer need to perform CAS operations. Its emergence is mainly due to the discovery that in some cases a lock is frequently acquired by the same thread. In this case, we can further optimize the lightweight lock.

Biased locks are actually designed for a single thread. When a thread acquires the lock for the first time, if no other thread acquires the lock, the thread holding the lock will no longer need to perform synchronization operations.

As you can see from the previous MarkWord structure, the biased lock will also record the thread ID through the CAS operation. If the same thread always acquires this lock, there is no need to perform additional CAS operations. Of course, if other threads come to grab it, the biased lock will decide whether to return to unlocked or expand to a lightweight lock based on the current status.

If we need to use biased locking, we can add -XX:+UseBiasedparameters to enable it.

Therefore, the final lock level is: Unlocked < Biased Lock < Lightweight Lock < Heavyweight Lock

It is worth noting that if the object hashCode()has calculated the consistent hash value of the object by calling a method, then it does not support biased locking and will directly enter the lightweight lock state, because the Hash needs to be saved, and biased locking The Mark Word data structure cannot save the Hash value; if the object is already in the biased lock state and then calls the hashCode()method, the lock will be directly upgraded to a heavyweight lock and the hash value will be stored in monitor(there is a reserved location for storage) .

Insert image description here

Lock elimination and lock coarsening

Lock elimination and lock coarsening are some optimization solutions at runtime. For example, although a certain section of our code is locked, there is no resource contention between threads at runtime. In this case, it is completely unnecessary. No locking mechanism is required, so the lock is removed. Lock coarsening means that mutually exclusive synchronization operations occur frequently in our code, such as locking inside a loop. This is obviously very performance-consuming, so once the virtual machine detects such an operation, it will expand the entire synchronization range.


JMM memory model

Note that the memory model mentioned here is not at the same level as the memory model we introduced in the JVM. The memory model in the JVM is the planning of the entire memory area by the virtual machine specification, and the Java memory model is above the JVM memory model. The abstract model and specific implementation are still based on the JVM memory model, which we will introduce later.

Java memory model

We 计算机组成原理have learned that in our CPU, there is usually a cache, and its appearance is to solve the problem that the speed of the memory cannot keep up with the processing speed of the processor, so one or more levels will be added inside the CPU. Cache is used to improve the data acquisition efficiency of the processor, but this will also lead to an obvious problem, because now basically all multi-core processors have their own cache, so how to ensure that Are the cache contents of each processor consistent?

Insert image description here

In order to solve the problem of cache consistency, each processor needs to follow some protocols when accessing the cache. When reading and writing, it must operate according to the protocol. Such protocols include MSI, MESI (Illinois Protocol), MOSI, Synapse, Firefly and Dragon. Protocol etc.

Java also uses a similar model to implement a memory model that supports multi-threading:

Insert image description here

The JMM (Java Memory Model) memory model is specified as follows:

  • All variables are stored in main memory (note that this includes the variables mentioned below, which refer to variables that will compete, including member variables, static variables, etc., and local variables are private to the thread and are not included)
  • Each thread has its own working memory (which can be compared to a CPU cache). All operations on variables by a thread must be performed in the working memory, and data in the main memory cannot be directly manipulated.
  • The working memory between different threads is isolated from each other. If content needs to be transferred between threads, it can only be done through the main memory and cannot directly access the other party's working memory.

In other words, if each thread wants to operate the data in the main memory, it must first copy it to its own working memory and operate on the copy of the data in the working memory. After the operation is completed, it also needs to copy the results from the working copy. Copy it back to the main memory. The specific operations are Save(save) and Load(load) operations.

So you will definitely be curious, how is this memory model implemented based on what was said about the JVM before?

  • Main memory: The part of the heap that stores instances of objects.
  • Working memory: Part of the virtual machine stack corresponding to the thread. The virtual machine may optimize this part of memory and place it in the CPU register or cache. For example, when accessing an array, since the array is a continuous memory space, part of the continuous space can be put into the CPU cache. Then if we read the array sequentially, there is a high probability that there will be a direct cache hit.

We mentioned earlier that cache inconsistency may be encountered in the CPU, and in Java, it may also be encountered, such as the following situation:

public class Main {
    
    
    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            for (int j = 0; j < 100000; j++) i++;
            System.out.println("线程1结束");
        }).start();
        new Thread(() -> {
    
    
            for (int j = 0; j < 100000; j++) i++;
            System.out.println("线程2结束");
        }).start();
        //等上面两个线程结束
        Thread.sleep(1000);
        System.out.println(i);
    }
}

It can be seen that two threads iperform 100,000 auto-increment operations on the variables at the same time, but the actual result is not what we expected.

So why is this happening? After studying the JVM before, I believe you should already know that the auto-increment operation is not actually completed by one instruction (be careful not to understand that a line of code is completed by one instruction):

Insert image description here

Including ithe acquisition, modification, and saving of variables, they are all split into operations one by one. Then it may happen that before the modification is saved, another thread has also saved it, but the current thread is unaware of it. .

Insert image description here

So, when we explained this problem in the JavaSE stage, we synchronizedsolved it by adding synchronized code blocks through keywords. Of course, we will explain other solutions (atomic classes) later.

Reorder

During compilation or execution, in order to optimize the execution efficiency of the program, the compiler or processor often reorders the instructions. The following situations occur:

  1. Compiler reordering: The Java compiler reorders code instructions according to optimization rules by understanding the semantics of Java code.
  2. Machine instruction level reordering: Modern processors are very advanced and can independently judge and change the execution order of machine instructions.

Instruction reordering can optimize the running efficiency of the program without changing the result (single thread), such as:

public static void main(String[] args) {
    
    
    int a = 10;
    int b = 20;
    System.out.println(a + b);
}

We can actually swap the first and second lines:

public static void main(String[] args) {
    
    
    int b = 10;
    int a = 20;
    System.out.println(a + b);
}

Even if an exchange occurs, the final running result of our program will not change. Of course, this is only demonstrated in the form of code. In fact, the JVM will also perform optimizations when executing bytecode instructions. The two instructions may not follow the original instructions. Some are done in order.

Although instruction rearrangement can indeed achieve a certain degree of optimization under single threads, it seems to cause some problems under multi-threads:

public class Main {
    
    
    private static int a = 0;
    private static int b = 0;
    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            if(b == 1) {
    
    
                if(a == 0) {
    
    
                    System.out.println("A");
                }else {
    
    
                    System.out.println("B");
                }   
            }
        }).start();
        new Thread(() -> {
    
    
            a = 1;
            b = 1;
        }).start();
    }
}

The above code, under normal circumstances and according to our normal thinking, is impossible to output A, because as long as b is equal to 1, then a must also be 1, because a is assigned before b. However, if reordering is performed, it is possible that the assignments of a and b are exchanged, and b is assigned a value of 1 first. At this time, thread 1 begins to determine whether b is 1, and a has not yet been assigned a value. Assigning a value of 1, thread 1 may have already gone to the printing place, so it is possible to output A.

volatile keyword

It’s been a long time since I’ve learned about new keywords. Today we’re going to look at a new keyword volatile. Before we start, let’s introduce three words:

  • Atomicity: In fact, I have said it many times before, that is, whatever you want to do is either done or not done. There is no such thing as half done.
  • Visibility: When multiple threads access the same variable, if one thread modifies the value of the variable, other threads can immediately see the modified value.
  • Orderliness: That is, the order of program execution is executed in the order of code.

As we said before, if multiple threads access the same variable, then the variable will be copied to its own working memory by the thread for operation, instead of directly operating on the variable itself in the main memory. The following operation seems to be a limited Loop, but infinitely:

public class Main {
    
    
    private static int a = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            while (a == 0);
            System.out.println("线程结束!");
        }).start();

        Thread.sleep(1000);
        System.out.println("正在修改a的值...");
        a = 1;   //很明显,按照我们的逻辑来说,a的值被修改那么另一个线程将不再循环
    }
}

In fact, this is what we said before. Although the value of a is modified in our main thread, the other thread does not know that the value of a has changed, so the old value is still used for judgment in the loop. Therefore, ordinary variables is not visible.

To solve this problem, the first thing we think of is to lock it. Only one thread can be used at the same time. This will work. Indeed, this can definitely solve the problem:

public class Main {
    
    
    private static int a = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            while (a == 0) {
    
    
                synchronized (Main.class){
    
    }
            }
            System.out.println("线程结束!");
        }).start();

        Thread.sleep(1000);
        System.out.println("正在修改a的值...");
        synchronized (Main.class){
    
    
            a = 1;
        }
    }
}

However, in addition to the solution of adding a lock, we can also use volatilekeywords to solve the problem. The first function of this keyword is to ensure the visibility of variables. When writing a volatilevariable, JMM will force the variable in the thread's local memory to be flushed to the main memory, and this write operation will cause the volatilevariable cache in other threads to be invalid. In this way, when another thread modifies this variable, The current thread will know immediately and update the variables in the working memory to the latest version.

So let’s try it out:

public class Main {
    
    
    //添加volatile关键字
    private static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            while (a == 0);
            System.out.println("线程结束!");
        }).start();

        Thread.sleep(1000);
        System.out.println("正在修改a的值...");
        a = 1;
    }
}

The result is exactly as we said, when a changes, the loop ends immediately.

Of course, although volatilevisibility can be guaranteed, atomicity cannot be guaranteed. To solve our above i++problem, based on the knowledge we have learned so far, we can only use locking to complete:

public class Main {
    
    
    private static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable r = () -> {
    
    
            for (int i = 0; i < 10000; i++) a++;
            System.out.println("任务完成!");
        };
        new Thread(r).start();
        new Thread(r).start();

        //等待线程执行完成
        Thread.sleep(1000);
        System.out.println(a);
    }
}

No, volatileisn't it visible to other threads when changing variables? So why can't it still guarantee atomicity? Again, the auto-increment operation is divided into multiple steps to complete. Although visibility is guaranteed, as long as the hand speed is fast enough, there will still be a problem of two threads writing the same value at the same time (for example, thread 1 has just written the same value. The value of a is updated to 100. At this time, thread 2 may have already executed the instruction to update the value of a, and cannot stop the car, so the value of a will still be updated to 100 again.)

So if we really encounter this situation, then we can't all write a lock, right? Later, we will introduce the atomic class to specifically solve this problem.

最后一个功能就是volatile会禁止指令重排,也就是说,如果我们操作的是一个volatile变量,它将不会出现重排序的情况,也就解决了我们最上面的问题。那么它是怎么解决的重排序问题呢?若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的顺序
  2. 保证某些变量的内存可见性(volatile的内存可见性,其实就是依靠这个实现的)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序。

Insert image description here

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 保证Load1的读取操作在Load2及后续读取操作之前执行
StoreStore Store1;StoreStore;Store2 在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存
LoadStore Load1;LoadStore;Store2 在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
StoreLoad Store1;StoreLoad;Load2 保证load1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

所以volatile能够保证,之前的指令一定全部执行,之后的指令一定都没有执行,并且前面语句的结果对后面的语句可见。

最后我们来总结一下volatile关键字的三个特性:

  • 保证可见性
  • 不保证原子性
  • 防止指令重排

在之后我们的设计模式系列视频中,还会讲解单例模式下volatile的运用。

happens-before原则

After our previous explanation, I believe you have understood the advantages and disadvantages brought by the JMM memory model and reordering mechanisms. In summary, JMM has proposed the happens-before(first-come-first-served) principle and defined some scenarios that prohibit compilation and optimization to inform your programs. Members must make some guarantees that as long as we program according to principles, we can maintain the correctness of concurrent programming. details as follows:

  • **Program sequence rules:** In the same thread, according to the order of the program, the previous operation happens-before any subsequent operation.
    • Within the same thread, the execution results of the code are ordered. In fact, instructions may be rearranged, but it is guaranteed that the execution result of the code must be consistent with that obtained by executing it in sequence. The modification of a certain variable in the program must be visible to subsequent operations. It is impossible to change a just before. Modify it to 1, and then read a, which is the result before the modification. This is also the most basic requirement for the program to run.
  • **Monitor lock rules:** The unlocking operation of a lock happens-before the subsequent locking operation of the lock.
    • That is, whether in a single-threaded environment or a multi-threaded environment, for the same lock, after one thread unlocks the lock, and another thread acquires the lock, it can see the operation results of the previous thread. For example, the previous thread xmodified the value of the variable to 12unlock it, and then another thread obtained the lock. The operations of the previous thread are visible, and the xmodified results of the previous thread can be obtained 12(so synchronized has happens-before regular)
  • **Volatile variable rules:** A write operation to a volatile variable happens-before a subsequent read operation to this variable.
    • That is, if one thread writes a volatilevariable first, and then another thread reads the variable, then the result of the write operation must be visible to the thread that read the variable.
  • **Thread startup rules:** Main thread A starts thread B. In thread B, you can see the operations before the main thread started B.
    • During the execution of main thread A, start sub-thread B, then the modification results of thread A to shared variables before starting sub-thread B are visible to thread B.
  • **Thread joining rules:** If thread A executes an operation join()on thread B and returns successfully, then any operation in thread B happens-before the join()operation on thread A returns successfully.
  • **Transitivity Rule:** If A happens-before B and B happens-before C, then A happens-before C.

So let’s explain the following program results from the perspective of the happens-before principle:

public class Main {
    
    
    private static int a = 0;
  	private static int b = 0;
    public static void main(String[] args) {
    
    
        a = 10;
        b = a + 1;
        new Thread(() -> {
    
    
          if(b > 10) System.out.println(a); 
        }).start();
    }
}

First we define the operations that appear above:

  • **A:** aModify the value of the variable to10
  • **B:** bModify the value of the variable toa + 1
  • **C:** The main thread starts a new thread, obtains it in the new thread b, makes a judgment, and 10prints it if it is greater thana

First, let's analyze. Since it is the same thread, and B is an assignment operation and reads A , then according to the program order rules , A happens-before B, and then after B, C is executed immediately. According to the thread startup rules , Before the new thread starts, all operations before the current thread are visible to the new thread, so B happens-before C. Finally, according to the transitivity rule , since A happens-before B, B happens-before C, so A happens -before C, so athe modified results are output in a new thread 10.

Multi-threaded programming core

Earlier, we learned about the underlying operating mechanism of multi-threading. We finally know that there are so many problems in a multi-threading environment. Before JDK5, we could only choose synchronizedkeywords to implement locks. After JDK5, because volatilekeywords have been upgraded (the specific functions are described in the previous chapter), the concurrency framework package appeared. Compared with traditional synchronizedkeywords , we have more choices for the implementation of locks.

Doug Lea — author of the JUC concurrency package

If the history of IT is connected by human beings, then Doug Lea will definitely be indispensable. This old man with glasses on his nose, a beard like King Wilhelm II of Germany, and a humble and shy smile always on his face, serves in the Computer Science Department of the State University of New York at Oswego.

It is no exaggeration to say that he is the person with the greatest influence on Java in the world. Because of the two major changes in Java history, he played a pivotal role indirectly or directly. Tiger launched in 2004. Tiger has adopted the syntax and standards of 15 JSRs (Java Specification Requests), one of which is JSR-166. JSR-166 comes from the util.concurrent package written by Doug.

So, starting from this chapter, let us feel what JUC has brought us.


lock frame

After JDK 5, the Lock interface (and related implementation classes) was added to the concurrency package to implement the lock function. The Lock interface provides a synchronization function similar to the synchronized keyword, but requires manual acquisition and release of the lock when using it.

Lock and Condition interface

Using locks in the concurrent package is different from our traditional synchronizedlocks. We can think of the lock here as a real lock. Each lock is a corresponding lock object. I only need to obtain the lock from the lock object or Just release the lock. Let's first take a look at what is defined in this interface:

public interface Lock {
    
    
  	//获取锁,拿不到锁会阻塞,等待其他线程释放锁,获取到锁后返回
    void lock();
  	//同上,但是等待过程中会响应中断
    void lockInterruptibly() throws InterruptedException;
  	//尝试获取锁,但是不会阻塞,如果能获取到会返回true,不能返回false
    boolean tryLock();
  	//尝试获取锁,但是可以限定超时时间,如果超出时间还没拿到锁返回false,否则返回true,可以响应中断
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  	//释放锁
    void unlock();
  	//暂时可以理解为替代传统的Object的wait()、notify()等操作的工具
    Condition newCondition();
}

Here we can demonstrate how to use the Lock class to perform locking and releasing lock operations:

public class Main {
    
    
    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        Lock testLock = new ReentrantLock();   //可重入锁ReentrantLock类是Lock类的一个实现,我们后面会进行介绍
        Runnable action = () -> {
    
    
            for (int j = 0; j < 100000; j++) {
    
       //还是以自增操作为例
                testLock.lock();    //加锁,加锁成功后其他线程如果也要获取锁,会阻塞,等待当前线程释放
                i++;
                testLock.unlock();  //解锁,释放锁之后其他线程就可以获取这把锁了(注意在这之前一定得加锁,不然报错)
            }
        };
        new Thread(action).start();
        new Thread(action).start();
        Thread.sleep(1000);   //等上面两个线程跑完
        System.out.println(i);
    }
}

It can be seen that synchronizedcompared with what we used before, we are really operating a "lock" object here. When we need to lock, we only need to call the lock()method, and when we need to release the lock, we only need to call unlock()the method. The final result of the program running synchronizedis the same as using a lock.

wait()So, how do we call the object's and methods like traditional locking notify()? The concurrent package provides the Condition interface:

public interface Condition {
    
    
  	//与调用锁对象的wait方法一样,会进入到等待状态,但是这里需要调用Condition的signal或signalAll方法进行唤醒(感觉就是和普通对象的wait和notify是对应的)同时,等待状态下是可以响应中断的
 		void await() throws InterruptedException;
  	//同上,但不响应中断(看名字都能猜到)
  	void awaitUninterruptibly();
  	//等待指定时间,如果在指定时间(纳秒)内被唤醒,会返回剩余时间,如果超时,会返回0或负数,可以响应中断
  	long awaitNanos(long nanosTimeout) throws InterruptedException;
  	//等待指定时间(可以指定时间单位),如果等待时间内被唤醒,返回true,否则返回false,可以响应中断
  	boolean await(long time, TimeUnit unit) throws InterruptedException;
  	//可以指定一个明确的时间点,如果在时间点之前被唤醒,返回true,否则返回false,可以响应中断
  	boolean awaitUntil(Date deadline) throws InterruptedException;
  	//唤醒一个处于等待状态的线程,注意还得获得锁才能接着运行
  	void signal();
  	//同上,但是是唤醒所有等待线程
  	void signalAll();
}

Here we demonstrate it with a simple example:

public static void main(String[] args) throws InterruptedException {
    
    
    Lock testLock = new ReentrantLock();
    Condition condition = testLock.newCondition();
    new Thread(() -> {
    
    
        testLock.lock();   //和synchronized一样,必须持有锁的情况下才能使用await
        System.out.println("线程1进入等待状态!");
        try {
    
    
            condition.await();   //进入等待状态
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程1等待结束!");
        testLock.unlock();
    }).start();
    Thread.sleep(100); //防止线程2先跑
    new Thread(() -> {
    
    
        testLock.lock();
        System.out.println("线程2开始唤醒其他等待线程");
        condition.signal();   //唤醒线程1,但是此时线程1还必须要拿到锁才能继续运行
        System.out.println("线程2结束");
        testLock.unlock();   //这里释放锁之后,线程1就可以拿到锁继续运行了
    }).start();
}

It can be found that the use of Condition objects is not very different from the use of traditional objects.

**Thinking:**What is the difference between the following situation and the above?

public static void main(String[] args) throws InterruptedException {
    
    
    Lock testLock = new ReentrantLock();
    new Thread(() -> {
    
    
        testLock.lock();
        System.out.println("线程1进入等待状态!");
        try {
    
    
            testLock.newCondition().await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程1等待结束!");
        testLock.unlock();
    }).start();
    Thread.sleep(100);
    new Thread(() -> {
    
    
        testLock.lock();
        System.out.println("线程2开始唤醒其他等待线程");
        testLock.newCondition().signal();
        System.out.println("线程2结束");
        testLock.unlock();
    }).start();
}

Through analysis, it can be found that after the call newCondition(), a new Condition object will be generated, and multiple Condition objects can exist in the same lock (in fact, the original lock mechanism can only have one waiting queue, but many can be created here. Condition to implement multiple waiting queues), and in the above example, different Condition objects are actually used. Only waiting and waking up operations on the same Condition object will be effective, and different Condition objects are calculated separately.

Finally, let’s explain the time unit, which is an enumeration class and is also located java.util.concurrentunder the package:

public enum TimeUnit {
    
    
    /**
     * Time unit representing one thousandth of a microsecond
     */
    NANOSECONDS {
    
    
        public long toNanos(long d)   {
    
     return d; }
        public long toMicros(long d)  {
    
     return d/(C1/C0); }
        public long toMillis(long d)  {
    
     return d/(C2/C0); }
        public long toSeconds(long d) {
    
     return d/(C3/C0); }
        public long toMinutes(long d) {
    
     return d/(C4/C0); }
        public long toHours(long d)   {
    
     return d/(C5/C0); }
        public long toDays(long d)    {
    
     return d/(C6/C0); }
        public long convert(long d, TimeUnit u) {
    
     return u.toNanos(d); }
        int excessNanos(long d, long m) {
    
     return (int)(d - (m*C2)); }
    },
  	//....

You can see that there are many time units, such as DAY, SECONDS, MINUTESetc. We can directly use them as time units. For example, if we want a thread to wait for 3 seconds, we can write it as follows:

public static void main(String[] args) throws InterruptedException {
    
    
    Lock testLock = new ReentrantLock();
    new Thread(() -> {
    
    
        testLock.lock();
        try {
    
    
            System.out.println("等待是否未超时:"+testLock.newCondition().await(1, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        testLock.unlock();
    }).start();
}

Of course, the tryLock method of the Lock class also supports the use of time units, and you can test it yourself. In addition to being represented as a unit of time, TimeUnit can also be converted between different units:

public static void main(String[] args) throws InterruptedException {
    
    
    System.out.println("60秒 = "+TimeUnit.SECONDS.toMinutes(60) +"分钟");
    System.out.println("365天 = "+TimeUnit.DAYS.toSeconds(365) +" 秒");
}

You can also use object methods more conveniently wait():

public static void main(String[] args) throws InterruptedException {
    
    
    synchronized (Main.class) {
    
    
        System.out.println("开始等待");
        TimeUnit.SECONDS.timedWait(Main.class, 3);   //直接等待3秒
        System.out.println("等待结束");
    }
}

We can also use it directly to perform hibernation operations:

public static void main(String[] args) throws InterruptedException {
    
    
    TimeUnit.SECONDS.sleep(1);  //休眠1秒钟
}

Reentrant lock

Earlier, we explained the two core interfaces of the lock framework, then let's take a look at the specific implementation class of the lock interface. We used ReentrantLock earlier, which is actually a type of lock called a reentrant lock. So this reentrant lock What does entering mean? To put it simply, the same thread can perform locking operations repeatedly:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    lock.lock();   //连续加锁2次
    new Thread(() -> {
    
    
        System.out.println("线程2想要获取锁");
        lock.lock();
        System.out.println("线程2成功获取到锁");
    }).start();
    lock.unlock();
    System.out.println("线程1释放了一次锁");
    TimeUnit.SECONDS.sleep(1);
    lock.unlock();
    System.out.println("线程1再次释放了一次锁");  //释放两次后其他线程才能加锁
}

It can be seen that the main thread has performed two locking operations in a row (this operation will not be blocked). If the current thread holds the lock, continuing to lock will not be blocked. Moreover, after locking several times, It must be unlocked several times, otherwise this thread still holds the lock. We can use getHoldCount()the method to view the number of locks of the current thread:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    lock.lock();
    System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());
    TimeUnit.SECONDS.sleep(1);
    lock.unlock();
    System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());
    TimeUnit.SECONDS.sleep(1);
    lock.unlock();
    System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());
}

It can be seen that when the lock is no longer held by any thread, the value is , and the query result 0through the method is .isLocked()false

In fact, if there is a thread holding the current lock, other threads will temporarily enter the waiting queue when acquiring the lock. We can getQueueLength()obtain an estimate of the number of waiting threads through the method:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    Thread t1 = new Thread(lock::lock), t2 = new Thread(lock::lock);;
    t1.start();
    t2.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("当前等待锁释放的线程数:"+lock.getQueueLength());
    System.out.println("线程1是否在等待队列中:"+lock.hasQueuedThread(t1));
    System.out.println("线程2是否在等待队列中:"+lock.hasQueuedThread(t2));
    System.out.println("当前线程是否在等待队列中:"+lock.hasQueuedThread(Thread.currentThread()));
}

We can use hasQueuedThread()methods to determine whether a thread is waiting to acquire the lock status.

Similarly, Condition can also be judged:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
    
    
       lock.lock();
        try {
    
    
            condition.await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        lock.unlock();
    }).start();
    TimeUnit.SECONDS.sleep(1);
    lock.lock();
    System.out.println("当前Condition的等待线程数:"+lock.getWaitQueueLength(condition));
    condition.signal();
    System.out.println("当前Condition的等待线程数:"+lock.getWaitQueueLength(condition));
    lock.unlock();
}

By using getWaitQueueLength()the method, you can check how many threads are currently waiting for the same Condition.

Fair lock and unfair lock

We learned earlier that if threads compete for the same lock, they will temporarily enter the waiting queue. So, the order in which multiple threads obtain locks must be determined based on the time when the thread calls the method. We can lock()see ReentrantLockthat In the constructor, it is written like this:

public ReentrantLock() {
    
    
    sync = new NonfairSync();   //看名字貌似是非公平的
}

In fact, locks are divided into fair locks and unfair locks. By default, the ReentrantLock we created uses unfair locks as the underlying lock mechanism. So what is a fair lock and what is an unfair lock?

  • Fair lock: Multiple threads obtain locks in the order in which they apply for locks. The threads will directly enter the queue to queue up, and they will always be the first in the queue to obtain the lock.
  • Unfair lock: When multiple threads want to acquire a lock, they will directly try to acquire it. If they cannot acquire it, they will enter the waiting queue. If they can acquire it, they will acquire the lock directly.

To put it simply, the fair lock will not allow you to jump in line, and everyone will queue up honestly; the unfair lock will allow you to jump in line, but whether the people in line will let you jump in line is another matter.

We can test the performance of fair locks and unfair locks:

public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

Here we choose to use the second construction method, and you can choose whether to implement fair lock:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantLock lock = new ReentrantLock(false);

    Runnable action = () -> {
    
    
        System.out.println("线程 "+Thread.currentThread().getName()+" 开始获取锁...");
        lock.lock();
        System.out.println("线程 "+Thread.currentThread().getName()+" 成功获取锁!");
        lock.unlock();
    };
    for (int i = 0; i < 10; i++) {
    
       //建立10个线程
        new Thread(action, "T"+i).start();
    }
}

Here we only need to compare 将在1秒后开始获取锁...whether 成功获取锁!the order of the sums is consistent. If they are consistent, it means that all threads queued up to acquire the locks in order. If not, it means that a thread must have jumped in the queue.

The running results show that in fair mode, it is indeed carried out in order, but in unfair mode, this situation usually occurs: the thread can grab the lock as soon as it starts to acquire it, and it has started long ago. The thread is still waiting, which is an obvious queue-jumping behavior.

So, the next question is, is fair lock necessarily fair under any circumstances? We will leave the discussion of this issue to the queue synchronizer.


read-write lock

In addition to reentrant locks, there is another type of lock called a read-write lock. Of course, it is not a lock specifically used for read and write operations. The difference between it and a reentrant lock is that a reentrant lock is a Exclusive lock, when one thread obtains the lock, another thread must wait for it to release the lock, otherwise it will not be allowed to acquire the lock. The read-write lock allows multiple threads to obtain the lock at the same time. It actually appears for the read-write scenario.

The read-write lock maintains a read lock and a write lock. The mechanisms of these two locks are different.

  • Read lock: When no thread occupies the write lock, multiple threads can add read locks at the same time.
  • Write lock: When no thread occupies the read lock, only one thread can add the write lock at the same time.

Read-write locks also have a dedicated interface:

public interface ReadWriteLock {
    
    
    //获取读锁
    Lock readLock();

  	//获取写锁
    Lock writeLock();
}

This interface has an implementation class ReentrantReadWriteLock (which implements the ReadWriteLock interface, not the Lock interface, and is not a lock itself). Note that when we operate ReentrantReadWriteLock, we cannot lock directly. Instead, we need to obtain a read lock or a write lock, and then lock operate:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.readLock().lock();
    new Thread(lock.readLock()::lock).start();
}

Here we lock the read lock. You can see that multiple threads can lock the read lock at the same time.

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.readLock().lock();
    new Thread(lock.writeLock()::lock).start();
}

A write lock cannot be added when there is a read lock, and vice versa:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.writeLock().lock();
    new Thread(lock.readLock()::lock).start();
}

Moreover, ReentrantReadWriteLock not only has the function of read-write lock, but also retains reentrant lock and fair/unfair mechanism. For example, the same thread can repeatedly lock the write lock, and all must be unlocked before the lock is actually released:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.writeLock().lock();
    lock.writeLock().lock();
    new Thread(() -> {
    
    
        lock.writeLock().lock();
        System.out.println("成功获取到写锁!");
    }).start();
    System.out.println("释放第一层锁!");
    lock.writeLock().unlock();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("释放第二层锁!");
    lock.writeLock().unlock();
}

Verify fairness and unfairness through previous examples:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

    Runnable action = () -> {
    
    
        System.out.println("线程 "+Thread.currentThread().getName()+" 将在1秒后开始获取锁...");
        lock.writeLock().lock();
        System.out.println("线程 "+Thread.currentThread().getName()+" 成功获取锁!");
        lock.writeLock().unlock();
    };
    for (int i = 0; i < 10; i++) {
    
       //建立10个线程
        new Thread(action, "T"+i).start();
    }
}

As can be seen, the results are consistent.

Lock downgrade and lock upgrade

Lock downgrade refers to the downgrade of write lock to read lock. When a thread holds a write lock, although other threads cannot add read locks, the thread itself can add read locks:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.writeLock().lock();
    lock.readLock().lock();
    System.out.println("成功加读锁!");
}

So, if we release the write lock after adding the write lock and the read lock at the same time, can other threads add the read lock together?

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.writeLock().lock();
    lock.readLock().lock();
    new Thread(() -> {
    
    
        System.out.println("开始加读锁!");
        lock.readLock().lock();
        System.out.println("读锁添加成功!");
    }).start();
    TimeUnit.SECONDS.sleep(1);
    lock.writeLock().unlock();    //如果释放写锁,会怎么样?
}

It can be seen that once the write lock is released, the main thread only has the read lock. Because the read lock can be shared by multiple threads, the second thread also adds a read lock at this time. This operation is called "lock downgrade" (note that it is not to release the write lock first and then the read lock, but to apply for the read lock and then release the write lock while holding the write lock)

Note that applying for a write lock while only holding a read lock is a "lock upgrade" and is not supported by ReentrantReadWriteLock:

public static void main(String[] args) throws InterruptedException {
    
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.readLock().lock();
    lock.writeLock().lock();
    System.out.println("所升级成功!");
}

You can see that the thread is directly stuck at the sentence of adding write lock.

Queue Synchronizer AQS

**Note:** It is very difficult. If you are not very familiar with the use of locks, it is recommended to read it later!

We learned about reentrant locks and read-write locks earlier, so what are their underlying implementation principles? This is another part of the matryoshka doll analysis that everyone wants to skip when they see it.

For example, if we execute the ReentrantLock lock()method, how is it executed internally?

public void lock() {
    
    
    sync.lock();
}

As you can see, it actually doesn't do anything internally, but it is handed over to the Sync object for execution. Moreover, not only this method, but many other methods rely on the Sync object for execution:

public void unlock() {
    
    
    sync.release(1);
}

So what does this Sync object do? It can be seen that fair locks and unfair locks are inherited from Sync, and Sync is inherited from AbstractQueuedSynchronizer, referred to as queue synchronizer:

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    
   //...
}

static final class NonfairSync extends Sync {
    
    }
static final class FairSync extends Sync {
    
    }

Therefore, to understand how it operates at the bottom level, you have to look at the queue synchronizer. Let’s start here!

underlying implementation

AbstractQueuedSynchronizer (hereinafter referred to as AQS) is the basis for implementing the lock mechanism. Its internal encapsulation includes lock acquisition, release, and waiting queue.

The basic functions of a lock (exclusive lock as an example) are to acquire the lock and release the lock. When the lock is occupied, other threads competing for it will enter the waiting queue. AQS has encapsulated these basic functions, of which the waiting queue is the core. Content, the waiting queue is implemented by a doubly linked list data structure. Each thread in the waiting state can be encapsulated into a node and put into a doubly linked list. The doubly linked list is operated in the form of a queue. It is like so:

Insert image description here

headThere is one field and one field in AQS tailthat record the head node and tail node of the doubly linked list respectively, and a series of subsequent operations are performed around this queue. Let’s first understand what each node contains:

//每个处于等待状态的线程都可以是一个节点,并且每个节点是有很多状态的
static final class Node {
    
    
  	//每个节点都可以被分为独占模式节点或是共享模式节点,分别适用于独占锁和共享锁
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

  	//等待状态,这里都定义好了
   	//唯一一个大于0的状态,表示已失效,可能是由于超时或中断,此节点被取消。
    static final int CANCELLED =  1;
  	//此节点后面的节点被挂起(进入等待状态)
    static final int SIGNAL    = -1;	
  	//在条件队列中的节点才是这个状态
    static final int CONDITION = -2;
  	//传播,一般用于共享锁
    static final int PROPAGATE = -3;

    volatile int waitStatus;    //等待状态值
    volatile Node prev;   //双向链表基操
    volatile Node next;
    volatile Thread thread;   //每一个线程都可以被封装进一个节点进入到等待队列
  
    Node nextWaiter;   //在等待队列中表示模式,条件队列中作为下一个结点的指针

    final boolean isShared() {
    
    
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
    
    
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {
    
    
    }

    Node(Thread thread, Node mode) {
    
    
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
    
    
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

At the beginning, both headand are the default values :tailnullstate0

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

Don’t worry that the doubly linked list will not be initialized. The initialization starts only when it is actually used. Regardless, let’s look at other initialization contents:

//直接使用Unsafe类进行操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//记录类中属性的在内存中的偏移地址,方便Unsafe类直接操作内存进行赋值等(直接修改对应地址的内存)
private static final long stateOffset;   //这里对应的就是AQS类中的state成员字段
private static final long headOffset;    //这里对应的就是AQS类中的head头结点成员字段
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
    
       //静态代码块,在类加载的时候就会自动获取偏移地址
    try {
    
    
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) {
    
     throw new Error(ex); }
}

//通过CAS操作来修改头结点
private final boolean compareAndSetHead(Node update) {
    
    
  	//调用的是Unsafe类的compareAndSwapObject方法,通过CAS算法比较对象并替换
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

//同上,省略部分代码
private final boolean compareAndSetTail(Node expect, Node update) {
    
    

private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) {
    
    

private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    
    

It can be found that the queue synchronizer uses the CAS algorithm, so it directly uses the Unsafe tool class. The Unsafe class provides CAS operation methods (Java cannot implement it, and the bottom layer is implemented by C++). All modifications to the member fields in the AQS class , all have corresponding CAS operation packages.

Now that we have a general understanding of its underlying operating mechanism, let's take a look at how this class is used. It provides some overridable methods (according to different lock types and mechanisms, you can freely customize the rules, and it is exclusive Both conventional and non-exclusive locks provide corresponding methods), as well as some already written template methods (template methods will call these rewriteable methods). To use this class, you only need to rewrite the rewriteable methods. And call the provided template method to implement the lock function (it will be easier to understand if you have studied the design pattern)

Let's look at overridable methods first:

//独占式获取同步状态,查看同步状态是否和参数一致,如果返没有问题,那么会使用CAS操作设置同步状态并返回true
protected boolean tryAcquire(int arg) {
    
    
    throw new UnsupportedOperationException();
}

//独占式释放同步状态
protected boolean tryRelease(int arg) {
    
    
    throw new UnsupportedOperationException();
}

//共享式获取同步状态,返回值大于0表示成功,否则失败
protected int tryAcquireShared(int arg) {
    
    
    throw new UnsupportedOperationException();
}

//共享式释放同步状态
protected boolean tryReleaseShared(int arg) {
    
    
    throw new UnsupportedOperationException();
}

//是否在独占模式下被当前线程占用(锁是否被当前线程持有)
protected boolean isHeldExclusively() {
    
    
    throw new UnsupportedOperationException();
}

It can be seen that these methods that need to be rewritten are thrown directly by default UnsupportedOperationException, which means that according to different lock types, we need to implement the corresponding methods. We can take a look at ReentrantLock (this type is globally exclusive). How fair lock is implemented with AQS:

static final class FairSync extends Sync {
    
    
    private static final long serialVersionUID = -3000897897090466540L;

  	//加锁操作调用了模板方法acquire
  	//为了防止各位绕晕,请时刻记住,lock方法一定是在某个线程下为了加锁而调用的,并且同一时间可能会有其他线程也在调用此方法
    final void lock() {
    
    
        acquire(1);
    }

    ...
}

Let's first take a look at what the locking operation does. The template method provided by AQS is directly called here acquire(). Let's take a look at its implementation details in the AQS class:

@ReservedStackAccess //这个是JEP 270添加的新注解,它会保护被注解的方法,通过添加一些额外的空间,防止在多线程运行的时候出现栈溢出,下同
public final void acquire(int arg) {
    
    
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))   //节点为独占模式Node.EXCLUSIVE
        selfInterrupt();
}

First, the method will be called tryAcquire()(implemented here by the FairSync class). If the attempt to add an exclusive lock fails (false is returned), it means that other threads may hold the exclusive lock at this time, so the current thread has to wait first, then it will be called addWaiter()Method adds the thread to the waiting queue:

private Node addWaiter(Node mode) {
    
    
    Node node = new Node(Thread.currentThread(), mode);
    // 先尝试使用CAS直接入队,如果这个时候其他线程也在入队(就是不止一个线程在同一时间争抢这把锁)就进入enq()
    Node pred = tail;
    if (pred != null) {
    
    
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
    
    
            pred.next = node;
            return node;
        }
    }
  	//此方法是CAS快速入队失败时调用
    enq(node);
    return node;
}

private Node enq(final Node node) {
    
    
  	//自旋形式入队,可以看到这里是一个无限循环
    for (;;) {
    
    
        Node t = tail;
        if (t == null) {
    
      //这种情况只能说明头结点和尾结点都还没初始化
            if (compareAndSetHead(new Node()))   //初始化头结点和尾结点
                tail = head;
        } else {
    
    
            node.prev = t;
            if (compareAndSetTail(t, node)) {
    
    
                t.next = node;
                return t;   //只有CAS成功的情况下,才算入队成功,如果CAS失败,那说明其他线程同一时间也在入队,并且手速还比当前线程快,刚好走到CAS操作的时候,其他线程就先入队了,那么这个时候node.prev就不是我们预期的节点了,而是另一个线程新入队的节点,所以说得进下一次循环再来一次CAS,这种形式就是自旋
            }
        }
    }
}

After understanding that addWaiter()the method will add the node to the waiting queue, let's look at it next. addWaiter()The node that has been added will be returned. acquireQueued()When the returned node is obtained, it will also enter the spin state and wait for wake-up (that is, it will start to enter the lock-taking process. ):

@ReservedStackAccess
final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        for (;;) {
    
    
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
    
       //可以看到当此节点位于队首(node.prev == head)时,会再次调用tryAcquire方法获取锁,如果获取成功,会返回此过程中是否被中断的值
                setHead(node);    //新的头结点设置为当前结点
                p.next = null; // 原有的头结点没有存在的意义了
                failed = false;   //没有失败
                return interrupted;   //直接返回等待过程中是否被中断
            }	
          	//依然没获取成功,
            if (shouldParkAfterFailedAcquire(p, node) &&   //将当前节点的前驱节点等待状态设置为SIGNAL,如果失败将直接开启下一轮循环,直到成功为止,如果成功接着往下
                parkAndCheckInterrupt())   //挂起线程进入等待状态,等待被唤醒,如果在等待状态下被中断,那么会返回true,直接将中断标志设为true,否则就是正常唤醒,继续自旋
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    
    
    LockSupport.park(this);   //通过unsafe类操作底层挂起线程(会直接进入阻塞状态)
    return Thread.interrupted();
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;   //已经是SIGNAL,直接true
    if (ws > 0) {
    
       //不能是已经取消的节点,必须找到一个没被取消的
        do {
    
    
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;   //直接抛弃被取消的节点
    } else {
    
    
        //不是SIGNAL,先CAS设置为SIGNAL(这里没有返回true因为CAS不一定成功,需要下一轮再判断一次)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;   //返回false,马上开启下一轮循环
}

Therefore, acquire()if the if condition in is true, then there is only one situation, that is, the waiting process is interrupted. In any other situation, the exclusive lock is successfully obtained, so when the waiting process is interrupted, the method will be called selfInterrupt():

static void selfInterrupt() {
    
    
    Thread.currentThread().interrupt();
}

Here is the interrupt signal sent directly to the current thread.

The LockSupport class was mentioned above, it is a tool class, we can also play with this parkand unpark:

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t = Thread.currentThread();  //先拿到主线程的Thread对象
    new Thread(() -> {
    
    
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
            System.out.println("主线程可以继续运行了!");
            LockSupport.unpark(t);
          	//t.interrupt();   发送中断信号也可以恢复运行
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }).start();
    System.out.println("主线程被挂起!");
    LockSupport.park();
    System.out.println("主线程继续运行!");
}

Here we have lock()finished explaining the implementation of the fair lock method (let me guess, you are already dizzy, right? The more you go to the source code, the more your basic knowledge is tested, and the foundation is shaken). Next, let’s look at the fair lock. method tryAcquire():

static final class FairSync extends Sync {
    
    
  	//可重入独占锁的公平实现
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
    
    
        final Thread current = Thread.currentThread();   //先获取当前线程的Thread对象
        int c = getState();     //获取当前AQS对象状态(独占模式下0为未占用,大于0表示已占用)
        if (c == 0) {
    
           //如果是0,那就表示没有占用,现在我们的线程就要来尝试占用它
            if (!hasQueuedPredecessors() &&    //等待队列是否不为空且当前线程没有拿到锁,其实就是看看当前线程有没有必要进行排队,如果没必要排队,就说明可以直接获取锁
                compareAndSetState(0, acquires)) {
    
       //CAS设置状态,如果成功则说明成功拿到了这把锁,失败则说明可能这个时候其他线程在争抢,并且还比你先抢到
                setExclusiveOwnerThread(current);    //成功拿到锁,会将独占模式所有者线程设定为当前线程(这个方法是父类AbstractOwnableSynchronizer中的,就表示当前这把锁已经是这个线程的了)
                return true;   //占用锁成功,返回true
            }
        }
        else if (current == getExclusiveOwnerThread()) {
    
       //如果不是0,那就表示被线程占用了,这个时候看看是不是自己占用的,如果是,由于是可重入锁,可以继续加锁
            int nextc = c + acquires;    //多次加锁会将状态值进行增加,状态值就是加锁次数
            if (nextc < 0)   //加到int值溢出了?
                throw new Error("Maximum lock count exceeded");
            setState(nextc);   //设置为新的加锁次数
            return true;
        }
        return false;   //其他任何情况都是加锁失败
    }
}

After understanding the implementation of fair lock, do you feel a little enlightened? Although the whole process is very complicated, it is still relatively simple as long as you clarify your ideas.

The locking process is OK. Let's look at the unlocking process. unlock()The method is implemented in AQS:

public void unlock() {
    
    
    sync.release(1);    //直接调用了AQS中的release方法,参数为1表示解锁一次state值-1
}
@ReservedStackAccess
public final boolean release(int arg) {
    
    
    if (tryRelease(arg)) {
    
       //和tryAcquire一样,也得子类去重写,释放锁操作
        Node h = head;    //释放锁成功后,获取新的头结点
        if (h != null && h.waitStatus != 0)   //如果新的头结点不为空并且不是刚刚建立的结点(初始状态下status为默认值0,而上面在进行了shouldParkAfterFailedAcquire之后,会被设定为SIGNAL状态,值为-1)
            unparkSuccessor(h);   //唤醒头节点下一个节点中的线程
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    
    
    // 将等待状态waitStatus设置为初始值0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //获取下一个结点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    
       //如果下一个结点为空或是等待状态是已取消,那肯定是不能通知unpark的,这时就要遍历所有节点再另外找一个符合unpark要求的节点了
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)   //这里是从队尾向前,因为enq()方法中的t.next = node是在CAS之后进行的,而 node.prev = t 是CAS之前进行的,所以从后往前一定能够保证遍历所有节点
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)   //要是找到了,就直接unpark,要是还是没找到,那就算了
        LockSupport.unpark(s.thread);
}

So let’s take a look at tryRelease()how the method is implemented. The specific implementation is in Sync:

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    
    
    int c = getState() - releases;   //先计算本次解锁之后的状态值
    if (Thread.currentThread() != getExclusiveOwnerThread())   //因为是独占锁,那肯定这把锁得是当前线程持有才行
        throw new IllegalMonitorStateException();   //否则直接抛异常
    boolean free = false;
    if (c == 0) {
    
      //如果解锁之后的值为0,表示已经完全释放此锁
        free = true;
        setExclusiveOwnerThread(null);  //将独占锁持有线程设置为null
    }
    setState(c);   //状态值设定为c
    return free;  //如果不是0表示此锁还没完全释放,返回false,是0就返回true
}

To sum up, let’s draw a complete flow chart:

Insert image description here

Here we only explain fair locks. Regarding unfair locks and read-write locks, viewers are invited to interpret them by themselves based on our previous ideas.

Is a fair lock necessarily fair?

We explained the implementation principle of fair lock earlier. Then, let's try to analyze. In the case of concurrency, is fair lock necessarily fair?

Let’s review tryAcquire()the implementation of the method again:

@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    
    
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    
    
        if (!hasQueuedPredecessors() &&   //注意这里,公平锁的机制是,一开始会查看是否有节点处于等待
            compareAndSetState(0, acquires)) {
    
       //如果前面的方法执行后发现没有等待节点,就直接进入占锁环节了
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Therefore, hasQueuedPredecessors()there is no room for any mistakes in this link, otherwise the fairness will be directly destroyed. If this situation occurs now:

Thread 1 already holds the lock. At this time, thread 2 comes to compete for the lock. When it reaches hasQueuedPredecessors(), it is judged that falsethread 2 continues to run, and then thread 2 must fail to acquire the lock (because the lock is occupied by thread 1 at this time) , so it enters the waiting queue:

private Node enq(final Node node) {
    
    
    for (;;) {
    
    
        Node t = tail;
        if (t == null) {
    
     // 线程2进来之后,肯定是要先走这里的,因为head和tail都是null
            if (compareAndSetHead(new Node()))
                tail = head;   //这里就将tail直接等于head了,注意这里完了之后还没完,这里只是初始化过程
        } else {
    
    
            node.prev = t;
            if (compareAndSetTail(t, node)) {
    
    
                t.next = node;
                return t;
            }
        }
    }
}

private Node addWaiter(Node mode) {
    
    
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
    
       //由于一开始head和tail都是null,所以线程2直接就进enq()了
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
    
    
            pred.next = node;
            return node;
        }
    }
    enq(node);   //请看上面
    return node;
}

Unfortunately, thread 3 also came to grab the lock at this time, and followed the normal process to hasQueuedPredecessors()the method, and in this method:

public final boolean hasQueuedPredecessors() {
    
    
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
  	//这里直接判断h != t,而此时线程2才刚刚执行完 tail = head,所以直接就返回false了
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

Therefore, thread 3 is ready to start the CAS operation at this time, and by chance, thread 1 releases the lock at this time. The current situation is that thread 3 directly starts CAS judgment, while thread 2 is still inserting the node state. The result can be imagined As we know, thread 3 actually got the lock first, which obviously violates the fair lock mechanism.

A picture is:

Insert image description here

Therefore, it all depends on whether it is fair or not hasQueuedPredecessors(), and this method can only guarantee that there will be no problems when there are nodes in the waiting queue. Therefore, a fair lock is truly fair only when there is a node in the waiting queue.

Condition implementation principle

Through the previous study, we know that the Condition class is actually used to replace the wait/notify operation of traditional objects. It can also implement the wait/notify mode, and multiple Condition objects can be created under the same lock. So let's take a look at how it is implemented. Let's first analyze it from a single Condition object:

In AQS, Condition has an implementation class ConditionObject, and here a linked list is also used to implement the condition queue:

public class ConditionObject implements Condition, java.io.Serializable {
    
    
    private static final long serialVersionUID = 1173984872572414699L;
    /** 条件队列的头结点 */
    private transient Node firstWaiter;
    /** 条件队列的尾结点 */
    private transient Node lastWaiter;
  
  	//...

Here, the Node class in AQS is used directly, but the nextWaiter field in the Node class is used to connect the node, and the status of the Node is CONDITION:

Insert image description here

We know that when a thread calls await()a method, it will enter a waiting state until other threads call signal()a method to wake it up, and the condition queue here is used to store these threads in a waiting state.

Let’s first take a look at how the most critical await()method is implemented. In order to prevent confusion, before we start, let’s clarify the goal of this method:

  • Only threads that already hold the lock can use this method
  • When this method is called, the lock will be released directly, no matter how many times the lock is added.
  • signal()The waiting thread will only be awakened when called by other threads or interrupted.
  • After being awakened, you need to wait for other threads to release the lock. After getting the lock, you can continue execution and return to the previous state (await added several layers of locks before waking up and still has several layers of locks)

Okay, it’s almost time to upload the source code:

public final void await() throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();   //如果在调用await之前就被添加了中断标记,那么会直接抛出中断异常
    Node node = addConditionWaiter();    //为当前线程创建一个新的节点,并将其加入到条件队列中
    int savedState = fullyRelease(node);    //完全释放当前线程持有的锁,并且保存一下state值,因为唤醒之后还得恢复
    int interruptMode = 0;     //用于保存中断状态
    while (!isOnSyncQueue(node)) {
    
       //循环判断是否位于同步队列中,如果等待状态下的线程被其他线程唤醒,那么会正常进入到AQS的等待队列中(之后我们会讲)
        LockSupport.park(this);   //如果依然处于等待状态,那么继续挂起
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)   //看看等待的时候是不是被中断了
            break;
    }
  	//出了循环之后,那线程肯定是已经醒了,这时就差拿到锁就可以恢复运行了
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  //直接开始acquireQueued尝试拿锁(之前已经讲过了)从这里开始基本就和一个线程去抢锁是一样的了
        interruptMode = REINTERRUPT;
  	//已经拿到锁了,基本可以开始继续运行了,这里再进行一下后期清理工作
    if (node.nextWaiter != null) 
        unlinkCancelledWaiters();  //将等待队列中,不是Node.CONDITION状态的节点移除
    if (interruptMode != 0)   //依然是响应中断
        reportInterruptAfterWait(interruptMode);
  	//OK,接着该干嘛干嘛
}

In fact, await()the method is quite satisfactory, and most of the operations are within our expectations. Let's take a look at signal()how the method is implemented. Similarly, in order to prevent you from getting confused, let's first clarify the goal of the signal:

  • Only the thread holding the lock can wake up the thread waiting for the Condition to which the lock belongs.
  • Prioritize the first one in the wake-up condition queue. If there is a problem during the wake-up process, then search down until you find one that can be woken up.
  • The wake-up operation is essentially to throw the nodes in the condition queue directly into the AQS waiting queue, allowing them to participate in the lock competition.
  • After getting the lock, the thread can resume running.

Insert image description here

Okay, on to the source code:

public final void signal() {
    
    
    if (!isHeldExclusively())    //先看看当前线程是不是持有锁的状态
        throw new IllegalMonitorStateException();   //不是?那你不配唤醒别人
    Node first = firstWaiter;    //获取条件队列的第一个结点
    if (first != null)    //如果队列不为空,获取到了,那么就可以开始唤醒操作
        doSignal(first);
}
private void doSignal(Node first) {
    
    
    do {
    
    
        if ( (firstWaiter = first.nextWaiter) == null)   //如果当前节点在本轮循环没有后继节点了,条件队列就为空了
            lastWaiter = null;   //所以这里相当于是直接清空
        first.nextWaiter = null;   //将给定节点的下一个结点设置为null,因为当前结点马上就会离开条件队列了
    } while (!transferForSignal(first) &&   //接着往下看
             (first = firstWaiter) != null);   //能走到这里只能说明给定节点被设定为了取消状态,那就继续看下一个结点
}
final boolean transferForSignal(Node node) {
    
    
    /*
     * 如果这里CAS失败,那有可能此节点被设定为了取消状态
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //CAS成功之后,结点的等待状态就变成了默认值0,接着通过enq方法直接将节点丢进AQS的等待队列中,相当于唤醒并且可以等待获取锁了
  	//这里enq方法返回的是加入之后等待队列队尾的前驱节点,就是原来的tail
    Node p = enq(node);
    int ws = p.waitStatus;   //保存前驱结点的等待状态
  	//如果上一个节点的状态为取消, 或者尝试设置上一个节点的状态为SIGNAL失败(可能是在ws>0判断完之后马上变成了取消状态,导致CAS失败)
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);  //直接唤醒线程
    return true;
}

In fact, the most puzzling thing is the second to last line. Obviously, the above lines have entered the AQS waiting queue normally, and the normal process should be started. So why do we need to unpark in advance here?

This is actually written for optimization. There will be two situations in directly unparking:

  • If the tail node of the AQS waiting queue has been canceled before inserting the node, then wc > 0 is satisfied.
  • If after inserting the node, the tail node of the internal waiting queue in AQS has stabilized and satisfies tail.waitStatus == 0, but is canceled before executing ws >
    0!compareAndSetWaitStatus(p, ws, Node.SIGNAL), CAS will also
    Failure, satisfy compareAndSetWaitStatus(p, ws,
    Node.SIGNAL) == false

If it is unparked in advance, await()it will be awakened directly in the method, jump out of the while loop, and start fighting for the lock directly, because the previous waiting node is in a canceled state, and there is no need to wait for it anymore.

So, here’s the rough process:

Insert image description here

As long as the whole process is clarified, it is easy to understand.

Implement your own lock class

Now that I have learned so many functions of AQS, I will imitate these lock classes to implement a simple lock:

  • Requirements: Only one thread can hold the lock at the same time, and reentrancy is not required (repeated locks can be ignored)
public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        
    }

    /**
     * 自行实现一个最普通的独占锁
     * 要求:同一时间只能有一个线程持有锁,不要求可重入
     */
    private static class MyLock implements Lock {
    
    

        /**
         * 设计思路:
         * 1. 锁被占用,那么exclusiveOwnerThread应该被记录,并且state = 1
         * 2. 锁没有被占用,那么exclusiveOwnerThread为null,并且state = 0
         */
        private static class Sync extends AbstractQueuedSynchronizer {
    
    
        
            @Override
            protected boolean tryAcquire(int arg) {
    
    
                if(isHeldExclusively()) return true;     //无需可重入功能,如果是当前线程直接返回true
                if(compareAndSetState(0, arg)){
    
        //CAS操作进行状态替换
                    setExclusiveOwnerThread(Thread.currentThread());    //成功后设置当前的所有者线程
                    return true;
                }
                return false;
            }

            @Override
            protected boolean tryRelease(int arg) {
    
    
                if(getState() == 0)
                    throw new IllegalMonitorStateException();   //没加锁情况下是不能直接解锁的
                if(isHeldExclusively()){
    
         //只有持有锁的线程才能解锁
                    setExclusiveOwnerThread(null);    //设置所有者线程为null
                    setState(0);    //状态变为0
                    return true;
                }
                return false;
            }

            @Override
            protected boolean isHeldExclusively() {
    
    
                return getExclusiveOwnerThread() == Thread.currentThread();
            }

            protected Condition newCondition(){
    
    
                return new ConditionObject();    //直接用现成的
            }
        }

        private final Sync sync = new Sync();

        @Override
        public void lock() {
    
    
            sync.acquire(1);
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
    
    
            sync.acquireInterruptibly(1);
        }

        @Override
        public boolean tryLock() {
    
    
            return sync.tryAcquire(1);
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    
    
            return sync.tryAcquireNanos(1, unit.toNanos(time));
        }

        @Override
        public void unlock() {
    
    
            sync.release(1);
        }

        @Override
        public Condition newCondition() {
    
    
            return sync.newCondition();
        }
    }
}
Test lock usage
public class Main {
    
    
	public static void main(String[] args) throws Exception {
    
    
        MyLock lock = new MyLock();
        lock.lock();

        new Thread(()->{
    
    
            System.out.println("线程2正在等待锁");
            lock.lock();
            System.out.println("线程2拿到了锁");
        }).start();

        TimeUnit.SECONDS.sleep(1);

        System.out.println("线程1释放了锁!");

        lock.unlock();

    }
}
/*(主线程先拿到了锁,线程2尝试拿锁会到同步队列中去,当主线程释放后,会唤醒线程2)
线程2正在等待锁
线程1释放了锁!
线程2拿到了锁
*/
Test lock condition queue
public class Main {
    
    
	public static void main(String[] args) throws Exception {
    
    
        MyLock lock = new MyLock();
        Condition condition = lock.newCondition();

        lock.lock();

        new Thread(()->{
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
                lock.lock();

                System.out.println("线程2开始唤醒主线程");

                condition.signal();
                lock.unlock();

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();

        System.out.println("线程1开始等待");
        condition.await();
        System.out.println("线程1结束等待");
        lock.unlock();
    }
}
/* (主线程先获取到锁,线程2要么已经开始抢锁(如果抢锁了,就会进入同步队列中等待),线程2要么还没开始抢锁,
    然后主线程调用await的时会完全释放锁,并且进入条件队列,并且会尝试唤醒同步队列的头节点的下一个有效节点,
    如果此时的线程2已经进入了同步队列,那么就会被唤醒,如果没有进同步队列,那么可以继续去抢锁了)


线程1开始等待
线程2开始唤醒主线程
线程1结束等待
*/

At this point, our explanation of the queue synchronizer AQS ends here. Of course, the entire mechanism of AQS is not just what we have explained. There are some contents that we have not mentioned. Please explore by yourself. There will be a lot of content. Full sense of accomplishment~


Atomic class

Earlier we explained the use and implementation principles of the lock framework. Although it is relatively complicated, there are still a lot of gains (mainly from observing the code written by the big guys). Let's talk about this part in a relaxed way.

We mentioned earlier that if we want to ensure i++atomicity, then our only option is to lock. So, besides locking, are there any other better solutions? JUC provides us with atomic classes, and the bottom layer uses the CAS algorithm, which is a simple, performance-efficient, and thread-safe way to update variables.

All atomic classes are located java.util.concurrent.atomicunder packages.

Introduction to Atomic Classes

Commonly used basic data classes have corresponding atomic class encapsulation:

  • AtomicInteger: Atomic update int
  • AtomicLong: Atomic update long
  • AtomicBoolean: Atomic update boolean

So, is there any difference in the use of atomic classes and ordinary basic classes? Let's first look at using a basic type under normal circumstances:

public class Main {
    
    
    public static void main(String[] args) {
    
    
        int i = 1;
        System.out.println(i++);
    }
}

Now we use the atomic class corresponding to the int type. How to write the same code:

public class Main {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger i = new AtomicInteger(1);
        System.out.println(i.getAndIncrement());  //如果想实现i += 2这种操作,可以使用 addAndGet() 自由设置delta 值
    }
}

We can encapsulate int values ​​into this class (note that the constructor must be called, it does not have a boxing mechanism like Integer), and obtain or auto-increment the encapsulated int value by calling the methods provided by this class. At first glance, this is just a basic type of packaging class, nothing advanced. Indeed, it really smells like a packaging class, but it is not just a simple packaging, its increment operation is atomic:

public class Main {
    
    
    private static AtomicInteger i = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable r = () -> {
    
    
            for (int j = 0; j < 100000; j++)
                i.getAndIncrement();
            System.out.println("自增完成!");
        };
        new Thread(r).start();
        new Thread(r).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(i.get());
    }
}

We also perform the auto-increment operation directly. We found that using atomic classes can ensure the atomicity of the auto-increment operation, just like we did with the lock earlier. How could it be so magical? Let’s take a look at how the bottom layer is implemented, and click directly from the construction method:

private volatile int value;

public AtomicInteger(int initialValue) {
    
    
    value = initialValue;
}

public AtomicInteger() {
    
    
}

As you can see, its bottom layer is relatively simple. In fact, it essentially encapsulates an volatileint value of type, which ensures visibility and will not cause problems during CAS operations.

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    
    
    try {
    
    
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
    
     throw new Error(ex); }
}

You can see that the topmost one uses a similar mechanism to AQS. Because the CAS algorithm is used to update the value of value, the offset address of the value field in the object must be calculated first. CAS can directly modify the memory at the corresponding location (visible The Unsafe class plays a huge role, and many low-level operations rely on it to complete)

Next, let’s look at how the auto-increment operation works:

public final int getAndIncrement() {
    
    
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

You can see that it is called here unsafe.getAndAddInt(). The matryoshka time is up. Let’s take a look at what is written in Unsafe:

public final int getAndAddInt(Object o, long offset, int delta) {
    
      //delta就是变化的值,++操作就是自增1
    int v;
    do {
    
    
      	//volatile版本的getInt()
      	//能够保证可见性
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));  //这里是开始cas替换int的值,每次都去拿最新的值去进行替换,如果成功则离开循环,不成功说明这个时候其他线程先修改了值,就进下一次循环再获取最新的值然后再cas一次,直到成功为止
    return v;
}

You can see that this is a do-whileloop, so what is this loop doing? It feels similar to the mechanism in the AQS queue we explained before. It also uses spin to continuously perform CAS operations until it succeeds.

Insert image description here

It can be seen that the bottom layer of the atomic class also uses the CAS algorithm to ensure atomicity, including getAndSet, getAndAddand other methods. The atomic class also directly provides CAS operation methods, which we can use directly:

public static void main(String[] args) throws InterruptedException {
    
    
    AtomicInteger integer = new AtomicInteger(10);
    System.out.println(integer.compareAndSet(30, 20));
    System.out.println(integer.compareAndSet(10, 20));
    System.out.println(integer);
}

If you want to set the value as a normal variable, you can use lazySet()a method, which does not require volatilean immediately visible mechanism.

AtomicInteger integer = new AtomicInteger(1);
integer.lazySet(2);

In addition to basic classes having atomic classes, basic types of array types also have atomic classes:

  • AtomicIntegerArray: Atomic update of int array
  • AtomicLongArray: Atomic update of long array
  • AtomicReferenceArray: Atomic update reference array

In fact, atomic arrays and atomic types are the same, but we can perform atomic operations on the elements in the array:

public static void main(String[] args) throws InterruptedException {
    
    
    AtomicIntegerArray array = new AtomicIntegerArray(new int[]{
    
    0, 4, 1, 3, 5});
    Runnable r = () -> {
    
    
        for (int i = 0; i < 100000; i++)
            array.getAndAdd(0, 1);
    };
    new Thread(r).start();
    new Thread(r).start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println(array.get(0));
}

After JDK8, DoubleAdderand were added LongAdder. In high concurrency situations, the LongAdderperformance of AtomicLongis better than that AtomicLongof CAS operations are performed, but when high concurrency occurs, AtomicLonga large number of loop operations will be performed to ensure synchronization, and LongAdderthe CAS operations on the value will be dispersed into CAS operations on cellsmultiple elements in the array (a Cell[] as is maintained internally) Array, each Cell has a long variable with an initial value of 0. When concurrency is high, CAS will be dispersed, that is, different threads can perform CAS auto-increment on different elements in the array, thus avoiding the need for all threads to perform CAS auto-increment on different elements in the array. CAS with the same value), just add the results together at the end.

Insert image description here

Use as follows:

public static void main(String[] args) throws InterruptedException {
    
    
    LongAdder adder = new LongAdder();
    Runnable r = () -> {
    
    
        for (int i = 0; i < 100000; i++)
            adder.add(1);
    };
    for (int i = 0; i < 100; i++) {
    
    
        new Thread(r).start();   //100个线程
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(adder.sum());   //最后求和即可
}
/* 这里使用LongAdder比起使用AtomicLong要快的多,
   因为AtomicLong当线程越多,在自旋时失败的线程就越多,
   就会有很多的尝试cas,因此使用LongAdder,每个线程只操作自己对应的cell,以此来减少失败
 */

Since the underlying source code is relatively complex, I won’t explain it here. Performance comparison between the two (CountDownLatch is used here, it is recommended to look at it after learning):

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println("使用AtomicLong的时间消耗:"+test2()+"ms");
        System.out.println("使用LongAdder的时间消耗:"+test1()+"ms");
    }

    private static long test1() throws InterruptedException {
    
    
        CountDownLatch latch = new CountDownLatch(100);
        LongAdder adder = new LongAdder();
        long timeStart = System.currentTimeMillis();
        Runnable r = () -> {
    
    
            for (int i = 0; i < 100000; i++)
                adder.add(1);
            latch.countDown();
        };
        for (int i = 0; i < 100; i++)
            new Thread(r).start();
        latch.await();
        return System.currentTimeMillis() - timeStart;
    }

    private static long test2() throws InterruptedException {
    
    
        CountDownLatch latch = new CountDownLatch(100);
        AtomicLong atomicLong = new AtomicLong();
        long timeStart = System.currentTimeMillis();
        Runnable r = () -> {
    
    
            for (int i = 0; i < 100000; i++)
                atomicLong.incrementAndGet();
            latch.countDown();
        };
        for (int i = 0; i < 100; i++)
            new Thread(r).start();
        latch.await();
        return System.currentTimeMillis() - timeStart;
    }
}

Except forBasic data types support atomic operationsIn addition, forReference types can also implement atomic operationsof:

public static void main(String[] args) throws InterruptedException {
    
    
    String a = "Hello";
    String b = "World";
    AtomicReference<String> reference = new AtomicReference<>(a);
    reference.compareAndSet(a, b);
    System.out.println(reference.get());
}

JUC also provides a field atomic updater, which can perform atomic operations on a specified field in the class (note that the volatile keyword must be added to the field):

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Student student = new Student();
        AtomicIntegerFieldUpdater<Student> fieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
        System.out.println(fieldUpdater.incrementAndGet(student));
    }

    public static class Student{
    
    
        volatile int age;
    }
}

Now that you know so many atomic classes, do you feel that it is easier to achieve the task of ensuring atomicity?

ABA problems and solutions

Let's imagine this scenario:

Insert image description here

Thread 1 and thread 2 began ato CAS modify the value of a at the same time, but thread 1 was faster. After modifying the value of a to 2, it changed it back to 1. At this time, thread 2 began to make a judgment and found the value of a. is 1, so the CAS operation was successful.

Obviously, the 1 here is no longer the 1 at the beginning, but the reassigned 1. This is also a problem with the CAS operation (lock-free is good, but there are many problems). It will only mechanically compare the current value. It is not the expected value, but it does not care whether the current value has been modified. This kind of problem is called ABAa problem.

So how to solve this ABAproblem? JUC provides a reference type with a version number. As long as the version number is recorded for each operation and the version number is not repeated, the ABA problem can be solved:

public static void main(String[] args) throws InterruptedException {
    
    
    String a = "Hello";
    String b = "World";
    AtomicStampedReference<String> reference = new AtomicStampedReference<>(a, 1);  //在构造时需要指定初始值和对应的版本号
    reference.attemptStamp(a, 2);   //可以中途对版本号进行修改,注意要填写当前的引用对象
    System.out.println(reference.compareAndSet(a, b, 2, 3));   //CAS操作时不仅需要提供预期值和修改值,还要提供预期版本号和新的版本号
}

So far, this is the end of the explanation about atomic classes.


concurrent container

Now that we’ve finished talking about the simple stuff, it’s time to talk about something a bit more difficult.

**Note:** The focus of this section is to explore how concurrent containers use lock mechanisms and algorithms to implement various rich functions. We will ignore the implementation details of some regular functions (such as how to insert elements and delete elements in a linked list), and focus more on Concurrent containers deal with the implementation of algorithms for concurrent scenarios (for example, what rules are followed for insertion operations in a multi-threaded environment)

In single-threaded mode, the containers provided by collection classes can be said to be very convenient. They can be used more or less in almost every project of ours. In the JavaSE stage, we explained to you the implementation principles of each collection class. , we have learned about data structures such as linked lists, sequence tables, hash tables, etc. So, can these data structures still work normally in a multi-threaded environment?

Are traditional containers thread safe?

Let’s test what will happen if 100 threads add elements to ArrayList at the same time:

public class Main {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        Runnable r = () -> {
    
    
            for (int i = 0; i < 100; i++)
                list.add("lbwnb");
        };
        for (int i = 0; i < 100; i++)
            new Thread(r).start();
      	TimeUnit.SECONDS.sleep(1);
        System.out.println(list.size());
    }
}

If nothing else, an error will definitely be reported:

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 73
	at java.util.ArrayList.add(ArrayList.java:465)
	at com.test.Main.lambda$main$0(Main.java:13)
	at java.lang.Thread.run(Thread.java:750)
Exception in thread "Thread-19" java.lang.ArrayIndexOutOfBoundsException: 1851
	at java.util.ArrayList.add(ArrayList.java:465)
	at com.test.Main.lambda$main$0(Main.java:13)
	at java.lang.Thread.run(Thread.java:750)
9773

So let’s take a look at what error was reported. From the stack trace information, we can see that there is a problem with the add method:

public boolean add(E e) {
    
    
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;   //这一句出现了数组越界
    return true;
}

In other words, at the same time, other threads are also frantically adding elements to the array, so it is possible that other threads may have inserted elements ensureCapacityInternalafter (confirming sufficient capacity) and elementData[size++] = e;before execution, causing the size value to exceed the array capacity. These problems that are impossible to occur in single-threaded situations slowly appear in multi-threaded situations.

Let’s take a look at the more commonly used HashMap?

public static void main(String[] args) throws InterruptedException {
    
    
    Map<Integer, String> map = new HashMap<>();
    for (int i = 0; i < 100; i++) {
    
    
        int finalI = i;
        new Thread(() -> {
    
    
            for (int j = 0; j < 100; j++)
                map.put(finalI * 1000 + j, "lbwnb");
        }).start();
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println(map.size());
}

After testing, we found that although no error was reported, the final result was not what we expected. In fact, it may cause a ring data structure to appear in the Entry object, causing an infinite loop.

Therefore, to use collection classes safely in a multi-threaded environment, we have to find a solution.

Introduction to concurrent containers

How can we solve the container problem under concurrency? Our first thought must be to add a synchronzedkeyword in front of the method, so that we won't be robbed. In the past, we could use Vector or Hashtable to solve the problem, but their efficiency was too low, and they relied entirely on locks to solve the problem. , so they are rarely used anymore and will not be explained here.

JUC provides containers dedicated to concurrent scenarios. For example, the ArrayList we just used cannot be used in a multi-threaded environment. We can replace it with the multi-threaded dedicated collection class provided by JUC:

public static void main(String[] args) throws InterruptedException {
    
    
    List<String> list = new CopyOnWriteArrayList<>();  //这里使用CopyOnWriteArrayList来保证线程安全
    Runnable r = () -> {
    
    
        for (int i = 0; i < 100; i++)
            list.add("lbwnb");
    };
    for (int i = 0; i < 100; i++)
        new Thread(r).start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println(list.size());
}

We found that after using it CopyOnWriteArrayList, the above problems never occurred again.

So how is it implemented? Let's first take a look at how it operates add():

public boolean add(E e) {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();   //直接加锁,保证同一时间只有一个线程进行添加操作
    try {
    
    
        Object[] elements = getArray();  //获取当前存储元素的数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);   //直接复制一份数组
        newElements[len] = e;   //修改复制出来的数组
        setArray(newElements);   //将元素数组设定为复制出来的数组
        return true;
    } finally {
    
    
        lock.unlock();
    }
}

You can see that the add operation is directly locked, and will first copy the array currently storing the elements, then modify the array, and then replace the array (CopyOnWrite). Next, let's look at the read operation:

public E get(int index) {
    
    
    return get(getArray(), index);
}

Therefore, CopyOnWriteArrayListthe read operation is not locked, but the write operation is locked, similar to the read-write lock mechanism we explained earlier. This ensures that there will be no problems with the write operation without losing read performance.

Next let's look at the concurrent container for HashMap ConcurrentHashMap:

public static void main(String[] args) throws InterruptedException {
    
    
    Map<Integer, String> map = new ConcurrentHashMap<>();
    for (int i = 0; i < 100; i++) {
    
    
        int finalI = i;
        new Thread(() -> {
    
    
            for (int j = 0; j < 100; j++)
                map.put(finalI * 100 + j, "lbwnb");
        }).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(map.size());
}

You can see that the ConcurrentHashMap here does not have the problems of the previous HashMap. Because threads will compete for the same lock, we learned a pressure dispersion idea when explaining LongAdder before. Since every thread wants to grab the lock, then I will simply create a few more locks so that each of you can can be obtained, so there will be no waiting problem. Before JDK7, the principle of ConcurrentHashMap is similar. It stores all the data in segments, and divides it into many segments first. Each segment is given a lock. When a thread occupies a lock for access, it will only occupy one of the locks, that is, only a small segment of data is locked, while other segments of data can still be accessed normally by other threads.

Insert image description here

Here we focus on how it is implemented after JDK8. It uses the CAS algorithm and the lock mechanism to implement it. Let's first review the structure of HashMap under JDK8:

Insert image description here

HashMap就是利用了哈希表,哈希表的本质其实就是一个用于存放后续节点的头结点的数组,数组里面的每一个元素都是一个头结点(也可以说就是一个链表),当要新插入一个数据时,会先计算该数据的哈希值,找到数组下标,然后创建一个新的节点,添加到对应的链表后面。当链表的长度达到8时,会自动将链表转换为红黑树,这样能使得原有的查询效率大幅度降低!当使用红黑树之后,我们就可以利用二分搜索的思想,快速地去寻找我们想要的结果,而不是像链表一样挨个去看。

又是基础不牢地动山摇环节,由于ConcurrentHashMap的源码比较复杂,所以我们先从最简单的构造方法开始下手:

Insert image description here

我们发现,它的构造方法和HashMap的构造方法有很大的出入,但是大体的结构和HashMap是差不多的,也是维护了一个哈希表,并且哈希表中存放的是链表或是红黑树,所以我们直接来看put()操作是如何实现的,只要看明白这个,基本上就懂了:

public V put(K key, V value) {
    
    
    return putVal(key, value, false);
}

//有点小乱,如果看着太乱,可以在IDEA中折叠一下代码块,不然有点难受
final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    
    if (key == null || value == null) throw new NullPointerException(); //键值不能为空,基操
    int hash = spread(key.hashCode());    //计算键的hash值,用于确定在哈希表中的位置
    int binCount = 0;   //一会用来记录链表长度的,忽略
    for (Node<K,V>[] tab = table;;) {
    
        //无限循环,而且还是并发包中的类,盲猜一波CAS自旋锁
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();    //如果数组(哈希表)为空肯定是要进行初始化的,然后再重新进下一轮循环
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
       //如果哈希表该位置为null,直接CAS插入结点作为头结即可(注意这里会将f设置当前哈希表位置上的头结点)
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))  
                break;                   // 如果CAS成功,直接break结束put方法,失败那就继续下一轮循环
        } else if ((fh = f.hash) == MOVED)   //头结点哈希值为-1,这里只需要知道是因为正在扩容即可
            tab = helpTransfer(tab, f);   //帮助进行迁移,完事之后再来下一次循环
        else {
    
         //特殊情况都完了,这里就该是正常情况了,
            V oldVal = null;
            synchronized (f) {
    
       //在前面的循环中f肯定是被设定为了哈希表某个位置上的头结点,这里直接把它作为锁加锁了,防止同一时间其他线程也在操作哈希表中这个位置上的链表或是红黑树
                if (tabAt(tab, i) == f) {
    
    
                    if (fh >= 0) {
    
        //头结点的哈希值大于等于0说明是链表,下面就是针对链表的一些列操作
                        ...实现细节略
                    } else if (f instanceof TreeBin) {
    
       //肯定不大于0,肯定也不是-1,还判断是不是TreeBin,所以不用猜了,肯定是红黑树,下面就是针对红黑树的情况进行操作
                      	//在ConcurrentHashMap并不是直接存储的TreeNode,而是TreeBin
                        ...实现细节略
                    }
                }
            }
          	//根据链表长度决定是否要进化为红黑树
            if (binCount != 0) {
    
    
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);   //注意这里只是可能会进化为红黑树,如果当前哈希表的长度小于64,它会优先考虑对哈希表进行扩容
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

怎么样,是不是感觉看着挺复杂,其实也还好,总结一下就是:

Insert image description here

我们接着来看看get()操作:

public V get(Object key) {
    
    
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());   //计算哈希值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
    
    
      	// 如果头结点就是我们要找的,那直接返回值就行了
        if ((eh = e.hash) == h) {
    
    
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
      	//要么是正在扩容,要么就是红黑树,负数只有这两种情况
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
      	//确认无误,肯定在列表里,开找
        while ((e = e.next) != null) {
    
    
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
  	//没找到只能null了
    return null;
}

综上,ConcurrentHashMap的put操作,实际上是对哈希表上的所有头结点元素分别加锁,理论上来说哈希表的长度很大程度上决定了ConcurrentHashMap在同一时间能够处理的线程数量,这也是为什么treeifyBin()会优先考虑为哈希表进行扩容的原因。显然,这种加锁方式比JDK7的分段锁机制性能更好。

其实这里也只是简单地介绍了一下它的运行机制,ConcurrentHashMap真正的难点在于扩容和迁移操作,我们主要了解的是他的并发执行机制,有关它的其他实现细节,这里暂时不进行讲解。

阻塞队列

除了我们常用的容器类之外,JUC还提供了各种各样的阻塞队列,用于不同的工作场景。

阻塞队列本身也是队列,但是它是适用于多线程环境下的,基于ReentrantLock实现的,它的接口定义如下:

public interface BlockingQueue<E> extends Queue<E> {
    
    

	/****************【入队】****************/

	//入队,如果队列已满,将会抛出IllegalStateException异常
   	boolean add(E e);

    //入队,如果队列已满,返回false, 否则返回true(非阻塞)
    boolean offer(E e);

    //入队,如果队列已满,阻塞线程直到能入队为止
    void put(E e) throws InterruptedException;

    //入队,如果队列已满,阻塞线程直到能入队或超时、中断为止,入队成功返回true否则false
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;


	/****************【出队】****************/
	
    //出队,如果队列为空,阻塞线程直到能出队为止
    // (与上面take()对应)
    E take() throws InterruptedException;

    //出队,如果队列为空,阻塞线程直到能出队超时、中断为止,出队成功正常返回,否则返回null
    // (与上面带参数的offer(e,timeout,unit)对应)
    E poll(long timeout, TimeUnit unit) throws InterruptedException;

	/****************【其它】****************/
	
    //返回此队列理想情况下(在没有内存或资源限制的情况下)可以不阻塞地入队的数量,如果没有限制,则返回 Integer.MAX_VALUE
    int remainingCapacity();

    boolean remove(Object o);

    public boolean contains(Object o);

  	//一次性从BlockingQueue中获取所有可用的数据对象(还可以指定获取数据的个数)
    int drainTo(Collection<? super E> c);

    int drainTo(Collection<? super E> c, int maxElements);

For example, there is a blocking queue with a capacity of 3. At this time, one thread putadds three elements to it, and the second thread then putadds three elements to it. At this time, because the capacity is full, it will be blocked directly, and this When the third thread takes 2 elements from the queue, thread 2 stops blocking and throws two elements in first. The other one still cannot enter, so it continues to block.

Insert image description here

Using blocking queues, we can easily implement consumer and producer patterns. Do you remember our actual combat in JavaSE?

The so-called producer-consumer model solves the strong coupling problem between producers and consumers through a container. In layman's terms, producers are constantly producing and consumers are constantly consuming, but the products consumed by consumers are produced by producers, so there must be an intermediate container. We can imagine this container as a shelf , when the shelf is empty, the producer wants to produce the product, and the consumer is waiting for the producer to produce the product on the shelf. When the shelf has goods, the consumer can take the goods from the shelf, and the producer is waiting. There are empty spaces on the shelves, and then the shelves are replenished, and the cycle continues.

Through multi-thread programming, we simulate two chefs and three customers in a restaurant. It is assumed that the chef takes 3 seconds to cook a dish, the customer takes 4 seconds to eat the dish, and only one dish can be placed on the window.

Let's take a look at how to implement it using a blocking queue. Here we use ArrayBlockingQueuethe implementation class:

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        BlockingQueue<Object> queue = new ArrayBlockingQueue<>(1);
        Runnable supplier = () -> {
    
    
            while (true){
    
    
                try {
    
    
                    String name = Thread.currentThread().getName();
                    System.err.println(time()+"生产者 "+name+" 正在准备餐品...");
                    TimeUnit.SECONDS.sleep(3);
                    System.err.println(time()+"生产者 "+name+" 已出餐!");
                    queue.put(new Object());
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    break;
                }
            }
        };
        Runnable consumer = () -> {
    
    
            while (true){
    
    
                try {
    
    
                    String name = Thread.currentThread().getName();
                    System.out.println(time()+"消费者 "+name+" 正在等待出餐...");
                    queue.take();
                    System.out.println(time()+"消费者 "+name+" 取到了餐品。");
                    TimeUnit.SECONDS.sleep(4);
                    System.out.println(time()+"消费者 "+name+" 已经将饭菜吃完了!");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    break;
                }
            }
        };
        for (int i = 0; i < 2; i++) new Thread(supplier, "Supplier-"+i).start();
        for (int i = 0; i < 3; i++) new Thread(consumer, "Consumer-"+i).start();
    }

    private static String time(){
    
    
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        return "["+format.format(new Date()) + "] ";
    }
}

It can be seen that the role of blocking queues in multi-threaded environments is very obvious. Counting ArrayBlockingQueue, there are three commonly used blocking queues:

  • ArrayBlockingQueue: Bounded buffer blocking queue (that is, the queue has a capacity limit. If it is full, it cannot be reinstalled. It can only block, array implementation)
  • SynchronousQueue: No buffering blocking queue (equivalent to ArrayBlockingQueue without capacity, so there is only blocking)
  • LinkedBlockingQueue: Unbounded buffer blocking queue (no capacity limit, capacity can also be limited, will also block, linked list implementation)

Here we take ArrayBlockingQueue as an example to interpret the source code. Let's first look at the construction method:

final ReentrantLock lock;

private final Condition notEmpty;

private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
    
    
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);   //底层采用锁机制保证线程安全性,这里我们可以选择使用公平锁或是非公平锁
    notEmpty = lock.newCondition();   //这里创建了两个Condition(都属于lock)一会用于入队和出队的线程阻塞控制
    notFull =  lock.newCondition();
}

Next let’s see how the putsum offermethod is implemented:

public boolean offer(E e) {
    
    
    checkNotNull(e);
    final ReentrantLock lock = this.lock;    //可以看到这里也是使用了类里面的ReentrantLock进行加锁操作
    lock.lock();    //保证同一时间只有一个线程进入
    try {
    
    
        if (count == items.length)   //直接看看队列是否已满,如果没满则直接入队,如果已满则返回false
            return false;
        else {
    
    
            enqueue(e);
            return true;
        }
    } finally {
    
    
        lock.unlock();
    }
}

public void put(E e) throws InterruptedException {
    
    
    checkNotNull(e);
    final ReentrantLock lock = this.lock;    //同样的,需要进行加锁操作
    lock.lockInterruptibly();    //注意这里是可以响应中断的
    try {
    
    
        while (count == items.length)
            notFull.await();    //可以看到当队列已满时会直接挂起当前线程,在其他线程出队操作时会被唤醒
        enqueue(e);   //直到队列有空位才将线程入队
    } finally {
    
    
        lock.unlock();
    }
}
private E dequeue() {
    
    
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();    //出队操作会调用notFull的signal方法唤醒被挂起处于等待状态的线程
    return x;
}

Next let’s look at the queue operation:

public E poll() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();    //出队同样进行加锁操作,保证同一时间只能有一个线程执行
    try {
    
    
        return (count == 0) ? null : dequeue();   //如果队列不为空则出队,否则返回null
    } finally {
    
    
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    
    
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();    //可以响应中断进行加锁
    try {
    
    
        while (count == 0)
            notEmpty.await();    //和入队相反,也是一直等直到队列中有元素之后才可以出队,在入队时会唤醒此线程
        return dequeue();
    } finally {
    
    
        lock.unlock();
    }
}
private void enqueue(E x) {
    
    
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();    //对notEmpty的signal唤醒操作
}

It can be seen that if you are very familiar with the use of locks, it will be very easy when reading these source codes.

Next, let's take a look at a special queue, SynchronousQueue, which has no capacity. That is to say, under normal circumstances, dequeueing must occur in pairs with enqueueing operations. Let's take a look at its interior first. We can see that there is an abstract class Transferer inside. , which defines a transfermethod:

abstract static class Transferer<E> {
    
    
    /**
     * 可以是put也可以是take操作
     *
     * @param e 如果不是空,即作为生产者,那么表示会将传入参数元素e交给消费者
     *          如果为空,即作为消费者,那么表示会从生产者那里得到一个元素e并返回
     * @param 是否可以超时
     * @param 超时时间
     * @return 不为空就是从生产者那里返回的,为空表示要么被中断要么超时。
     */
    abstract E transfer(E e, boolean timed, long nanos);
}

At first glance, it seems a bit confusing. Do we still need to rely on this thing to implement put and take operations? In fact, it is performed directly in the producer-consumer mode. Since there is no need to rely on any container structure to temporarily store data, we can directly transfertransfer data between producers and consumers through methods.

For example, if a thread puts a new element in, if no other thread calls the take method to obtain the element, it will continue to be blocked until a thread takes out the element, and it is necessary to wait for both the producer and the consumer to arrive before proceeding transfer. When handing over work, only one of the parties needs to wait.

public void put(E e) throws InterruptedException {
    
    
    if (e == null) throw new NullPointerException();  //判空
    if (transferer.transfer(e, false, 0) == null) {
    
       //直接使用transfer方法进行数据传递
        Thread.interrupted();    //为空表示要么被中断要么超时
        throw new InterruptedException();
    }
}

It has two implementations in fair and unfair modes. Here we look at how SynchronousQueue is implemented in fair mode:

static final class TransferQueue<E> extends Transferer<E> {
    
    
     //头结点(头结点仅作为头结点,后续节点才是真正等待的线程节点)
     transient volatile QNode head;
     //尾结点
     transient volatile QNode tail;

    /** 节点有生产者和消费者角色之分 */
    static final class QNode {
    
    
        volatile QNode next;          // 后继节点
        volatile Object item;         // 存储的元素
        volatile Thread waiter;       // 处于等待的线程,和之前的AQS一样的思路,每个线程等待的时候都会被封装为节点
        final boolean isData;         // 是生产者节点还是消费者节点

In fair mode, the implementation of Transferer is TransferQueue, which is based on the first-in-first-out rule. There is an internal QNode class to save waiting threads.

Okay, let’s go directly to transfer()the implementation of the method (I remind you again that source code analysis in a multi-threaded environment is different from single-thread analysis. We need to always pay attention to the lock status of the current code block. If it is not locked, it must have multiple The awareness that threads may run at the same time will accompany you when you deal with multi-threading issues in the future, so as to ensure that your ideas are correct in a multi-threaded environment):

E transfer(E e, boolean timed, long nanos) {
    
       //注意这里面没加锁,肯定会多个线程之间竞争
    QNode s = null;
    boolean isData = (e != null);   //e为空表示消费者,不为空表示生产者

    for (;;) {
    
    
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)         // 头结点尾结点任意为空(但是在构造的时候就已经不是空了)
            continue;                       // 自旋

        if (h == t || t.isData == isData) {
    
     // 头结点等于尾结点表示队列中只有一个头结点,肯定是空,或者尾结点角色和当前节点一样,这两种情况下,都需要进行入队操作
            QNode tn = t.next;
            if (t != tail)                  // 如果这段时间内t被其他线程修改了,如果是就进下一轮循环重新来
                continue;
            if (tn != null) {
    
                   // 继续校验是否为队尾,如果tn不为null,那肯定是其他线程改了队尾,可以进下一轮循环重新来了
                advanceTail(t, tn);					// CAS将新的队尾节点设置为tn,成不成功都无所谓,反正这一轮肯定没戏了
                continue;
            }
            if (timed && nanos <= 0)        // 超时返回null
                return null;
            if (s == null)
                s = new QNode(e, isData);   //构造当前结点,准备加入等待队列
            if (!t.casNext(null, s))        // CAS添加当前节点为尾结点的下一个,如果失败肯定其他线程又抢先做了,直接进下一轮循环重新来
                continue;

            advanceTail(t, s);              // 上面的操作基本OK了,那么新的队尾元素就修改为s
            Object x = awaitFulfill(s, e, timed, nanos);   //开始等待s所对应的消费者或是生产者进行交接,比如s现在是生产者,那么它就需要等到一个消费者的到来才会继续(这个方法会先进行自旋等待匹配,如果自旋一定次数后还是没有匹配成功,那么就挂起)
            if (x == s) {
    
                       // 如果返回s本身说明等待状态下被取消
                clean(t, s);
                return null;
            }

            if (!s.isOffList()) {
    
               // 如果s操作完成之后没有离开队列,那么这里将其手动丢弃
                advanceHead(t, s);          // 将s设定为新的首节点(注意头节点仅作为头结点,并非处于等待的线程节点)
                if (x != null)              // 删除s内的其他信息
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;   //假如当前是消费者,直接返回x即可,x就是从生产者那里拿来的元素

        } else {
    
                                // 这种情况下就是与队列中结点类型匹配的情况了(注意队列要么为空要么只会存在一种类型的节点,因为一旦出现不同类型的节点马上会被交接掉)
            QNode m = h.next;               // 获取头结点的下一个接口,准备进行交接工作
            if (t != tail || m == null || h != head)
                continue;                   // 判断其他线程是否先修改,如果修改过那么开下一轮

            Object x = m.item;
            if (isData == (x != null) ||    // 判断节点类型,如果是相同的操作,那肯定也是有问题的
                x == m ||                   // 或是当前操作被取消
                !m.casItem(x, e)) {
    
             // 上面都不是?那么最后再进行CAS替换m中的元素,成功表示交接成功,失败就老老实实重开吧
                advanceHead(h, m);          // dequeue and retry
                continue;
            }

            advanceHead(h, m);              // 成功交接,新的头结点可以改为m了,原有的头结点直接不要了
            LockSupport.unpark(m.waiter);   // m中的等待交接的线程可以继续了,已经交接完成
            return (x != null) ? (E)x : e;  // 同上,该返回什么就返回什么
        }
    }
}

Therefore, it can be summarized as the following process:

Insert image description here

For SynchronousQueue in unfair mode, a stack structure is used to store waiting nodes, but the idea is the same as here. It needs to wait and perform matching operations. If you are interested, you can continue to learn about the implementation of SynchronousQueue in unfair mode.

In JDK7, a more powerful TransferQueue was generated based on SynchronousQueue, which retained the matching and handover mechanism of SynchronousQueue and was integrated with the waiting queue.

We know that SynchronousQueue does not use locks, but uses CAS operations to ensure coordination between producers and consumers, but it has no capacity. Although LinkedBlockingQueue has capacity and is unbounded, it is basically implemented based on locks internally, and its performance is not Not very good. At this time, we can take out their respective advantages and rub them together to form a LinkedTransferQueue with higher performance.

public static void main(String[] args) throws InterruptedException {
    
    
    LinkedTransferQueue<String> queue = new LinkedTransferQueue<>();
    queue.put("1");  //插入时,会先检查是否有其他线程等待获取,如果是,直接进行交接,否则插入到存储队列中
   	queue.put("2");  //不会像SynchronousQueue那样必须等一个匹配的才可以
    queue.forEach(System.out::println);   //直接打印所有的元素,这在SynchronousQueue下只能是空,因为单独的入队或出队操作都会被阻塞
}

In comparison SynchronousQueue, it has one more queue that can be stored. We can still get the values ​​of all elements in the queue like a blocking queue. To put it simply, it LinkedTransferQueueis actually one more storage queue SynchronousQueue.

Next let's take a look at some other queues:

  • PriorityBlockingQueue - is a blocking queue that supports priority, and the order in which elements are obtained is determined by priority.
  • DelayQueue - It can implement delayed acquisition of elements and also supports priority.

Let’s first look at the priority blocking queue:

public static void main(String[] args) throws InterruptedException {
    
    
    PriorityBlockingQueue<Integer> queue =
            new PriorityBlockingQueue<>(10, Integer::compare);   //可以指定初始容量(可扩容)和优先级比较规则,这里我们使用升序
    queue.add(3);
    queue.add(1);
    queue.add(2);
    System.out.println(queue);    //注意保存顺序并不会按照优先级排列,所以可以看到结果并不是排序后的结果
    System.out.println(queue.poll());   //但是出队顺序一定是按照优先级进行的
    System.out.println(queue.poll());
    System.out.println(queue.poll());

}
/* 输出如下:
[1, 3, 2]
1
2
3
*/

Our focus is DelayQueue, which can achieve delayed dequeue. That is to say, when an element is inserted, if it does not exceed a certain time, the element cannot be dequeued.

public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
    
    

You can see that this class only accepts the implementation class of Delayed as an element:

public interface Delayed extends Comparable<Delayed> {
    
      //注意这里继承了Comparable,它支持优先级

    //获取剩余等待时间,正数表示还需要进行等待,0或负数表示等待结束
    long getDelay(TimeUnit unit);
}

Here we implement one manually:

private static class Test implements Delayed {
    
    
    private final long time;   //延迟时间,这里以毫秒为单位
    private final int priority;
    private final long startTime;
    private final String data;

    private Test(long time, int priority, String data) {
    
    
    	// 将指定time秒转为毫秒
        this.time = TimeUnit.SECONDS.toMillis(time);   //秒转换为毫秒
        this.priority = priority;
        this.startTime = System.currentTimeMillis();   //这里我们以毫秒为单位
        this.data = data;
    }

    @Override
    public long getDelay(TimeUnit unit) {
    
    
    	// System.currentTimeMillis()获取当前毫秒数
        long leftTime = time - (System.currentTimeMillis() - startTime); //计算剩余时间 = 设定时间 - 已度过时间(= 当前时间 - 开始时间)
        // 将leftTime毫秒转为指定时间单位的值
        return unit.convert(leftTime, TimeUnit.MILLISECONDS);   //注意进行单位转换,单位由队列指定(默认是纳秒单位)
    }

    @Override
    public int compareTo(Delayed o) {
    
    
        if(o instanceof Test)
            return priority - ((Test) o).priority;   //优先级越小越优先
        return 0;
    }

    @Override
    public String toString() {
    
    
        return data;
    }
}

Then we try to use it in the main method:

public static void main(String[] args) throws InterruptedException {
    
    
    DelayQueue<Test> queue = new DelayQueue<>();
    queue.add(new Test(1, 2, "2号"));   //1秒钟延时
    queue.add(new Test(3, 1, "1号"));   //3秒钟延时,优先级最高

    System.out.println(queue.take());    //注意出队顺序是依照优先级来的,即使一个元素已经可以出队了,依然需要等待优先级更高的元素到期
    System.out.println(queue.take());
}

Let's study how DelayQueue is implemented. First, let's look at add()the method:

public boolean add(E e) {
    
    
    return offer(e);
}

public boolean offer(E e) {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        q.offer(e);   //注意这里是向内部维护的一个优先级队列添加元素,并不是DelayQueue本身存储元素
        if (q.peek() == e) {
    
       //如果入队后队首就是当前元素,那么直接进行一次唤醒操作(因为有可能之前就有其他线程等着take了)
            leader = null;
            available.signal();
        }
        return true;
    } finally {
    
    
        lock.unlock();
    }
}

public void put(E e) {
    
    
    offer(e);
}

It can be seen that no matter what kind of enqueue operation is performed, it will be locked and is a normal operation. Let’s look at the method next take():

public E take() throws InterruptedException {
    
    
    final ReentrantLock lock = this.lock;   //出队也要先加锁,基操
    lock.lockInterruptibly();
    try {
    
    
        for (;;) {
    
        //无限循环,常规操作
            E first = q.peek();    //获取队首元素
            if (first == null)     //如果为空那肯定队列为空,先等着吧,等有元素进来
                available.await();
            else {
    
    
                long delay = first.getDelay(NANOSECONDS);    //获取延迟,这里传入的时间单位是纳秒
                if (delay <= 0)
                    return q.poll();     //如果获取到延迟时间已经小于0了,那说明ok,可以直接出队返回
                first = null;
                if (leader != null)   //这里用leader来减少不必要的等待时间,如果不是null那说明有线程在等待,为null说明没有线程等待
                    available.await();   //如果其他线程已经在等元素了,那么当前线程直接进永久等待状态
                else {
    
    
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;    //没有线程等待就将leader设定为当前线程
                    try {
    
    
                        available.awaitNanos(delay);     //获取到的延迟大于0,那么就需要等待延迟时间,再开始下一次获取
                    } finally {
    
    
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
    
    
        if (leader == null && q.peek() != null)
            available.signal();   //当前take结束之后唤醒一个其他永久等待状态下的线程
        lock.unlock();   //解锁,完事
    }
}

At this point, the explanation about concurrent containers ends here.

In the next chapter, we will continue to explain thread pools and concurrency tool classes.

Advanced Concurrent Programming

Welcome to the last chapter of JUC study. Of course Wang Zha is at the end.

Thread Pool

In our programs, multi-threading technology will be used more or less, and we used to use the Thread class to create a new thread:

public static void main(String[] args) {
    
    
    Thread t = new Thread(() -> System.out.println("Hello World!"));
    t.start();
}

Using multi-threading, our program can use CPU multi-core resources more rationally and complete more work at the same time. However, if our program frequently creates threads, since the creation and destruction of threads also requires system resources, this will reduce the performance of our entire program. So what can we do to use multi-threading more efficiently?

We can actually reuse the created threads and use pooling technology. Just like the database connection pool, we can also create many threads and then use these threads repeatedly without destroying them.

Although this idea sounds relatively novel, in fact thread pools have already been used in various places. For example, our Tomcat server needs to accept and process a large number of requests at the same time, so a large number of threads must be created in a short period of time. Then destroy it, which will obviously cause a lot of overhead, so using a thread pool is obviously a better solution in this case.

Because the thread pool can repeatedly use existing threads to perform multi-threaded operations, it generally has a capacity limit. When all threads are in working status, new multi-thread requests will be blocked until a thread is free. , in fact, the blocking queue we explained before will be used here.

So we can temporarily get the following look:
Insert image description here

Of course, the thread pool provided by JUC is certainly not that simple. Let us learn more about it next.

Use of thread pool

We can directly create a new thread pool object, which has already helped us implement the thread scheduling mechanism in advance. Let's first look at its construction method:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

There are a little more parameters, so here we explain them one by one:

  • corePoolSize: core thread pool size . Every time we submit a multi-threaded task to the thread pool, a new one will be created 核心线程, regardless of whether there are other idle threads, until the core thread pool size is reached, and then we will try to reuse thread resources. Of course, you can also initialize everything at the beginning and prestartAllCoreThreads()just call it.
  • maximumPoolSize: Maximum thread pool size . When all threads in the current thread pool are running and the waiting queue is full, it will directly try to continue creating new 非核心线程runs, but it cannot exceed the maximum thread pool size.
  • keepAliveTime: The maximum idle time of a thread . When an 非核心线程idle time exceeds a certain period, it will be automatically destroyed.
  • unit: the time unit of the thread’s maximum idle time
  • workQueue: Thread waiting queue . When the number of core threads in the thread pool is full, the task will be temporarily stored in the waiting queue until thread resources are available. The blocking queue we learned in the previous chapter can be used here.
  • threadFactory: Thread creation factory , we can interfere with the thread creation process in the thread pool and customize it.
  • handler: Rejection strategy . When there is no space in the waiting queue and thread pool, and no new tasks can really come, and a new multi-threaded task comes, then it can only be rejected. At this time, the rejection will be based on the current settings. strategy to handle.

The most important thing is the limit of the thread pool size. This is also very knowledgeable. Reasonable allocation of the size will make the execution efficiency of the thread pool twice the result with half the effort:

  • First, we can analyze the characteristics of the thread pool execution task, whether it is CPU-intensive or IO-intensive.
    • **CPU-intensive:** Mainly performs computing tasks, the response time is very fast, and the CPU is always running. This kind of task has a high CPU utilization, so the number of threads should be determined based on the number of CPU cores. The number of CPU cores = The maximum number of threads executing simultaneously, taking the i5-9400F processor as an example, the number of CPU cores is 6, then up to 6 threads can be executed simultaneously.
    • **IO-intensive:** Mainly perform IO operations, because the time to perform IO operations is relatively long, such as reading data from the hard disk, the CPU has to wait for IO operations, and it is easy to become idle, causing the CPU to The utilization rate is not high. In this case, the size of the thread pool can be appropriately increased so that more threads can perform IO operations together. Generally, it can be configured to 2 times the number of CPU cores.

Here we manually create a new thread pool to see the effect:

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor =
            new ThreadPoolExecutor(2, 4,   //2个核心线程,最大线程数为4个
                    3, TimeUnit.SECONDS,        //最大空闲时间为3秒钟
                    new ArrayBlockingQueue<>(2));     //这里使用容量为2的ArrayBlockingQueue队列

    for (int i = 0; i < 6; i++) {
    
       //开始6个任务
        int finalI = i;
        executor.execute(() -> {
    
    
            try {
    
    
                System.out.println(Thread.currentThread().getName()+" 开始执行!("+ finalI);
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+" 已结束!("+finalI);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });
    }

    TimeUnit.SECONDS.sleep(1);    //看看当前线程池中的线程数量
    System.out.println("线程池中线程数量:"+executor.getPoolSize());
    TimeUnit.SECONDS.sleep(5);     //等到超过空闲时间
    System.out.println("线程池中线程数量:"+executor.getPoolSize());

    executor.shutdownNow();    //使用完线程池记得关闭,不然程序不会结束,它会取消所有等待中的任务以及试图中断正在执行的任务,关闭后,无法再提交任务,一律拒绝
  	//executor.shutdown();     //同样可以关闭,但是会执行完等待队列中的任务再关闭
}

Here we create a thread pool with a core capacity of 2, a maximum capacity of 4, a waiting queue length of 2, and an idle time of 3 seconds. Now we execute 6 tasks in it, and each task will sleep for 1 second, then When both core threads in the thread pool are occupied, the remaining 4 threads can only enter the waiting queue, but there are only 2 capacities in the waiting queue. At this time, the thread pool will directly process the next two tasks. Try to create a thread, it can be created successfully since it is not larger than the maximum capacity. After all the threads were finally completed, after waiting for 5 seconds, they exceeded the maximum idle time of the thread pool and 非核心线程were recycled, so only 2 threads existed in the thread pool.

So what happens if the waiting queue is set to a SynchronousQueue with no capacity?

pool-1-thread-1 开始执行!(0
pool-1-thread-4 开始执行!(3
pool-1-thread-3 开始执行!(2
pool-1-thread-2 开始执行!(1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.test.Main$$Lambda$1/1283928880@682a0b20 rejected from java.util.concurrent.ThreadPoolExecutor@3d075dc0[Running, pool size = 4, active threads = 4, queued tasks = 0, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.test.Main.main(Main.java:15)

It can be seen that the first four tasks can be executed normally, but when it comes to the fifth task, an exception is thrown directly. This is actually because the capacity of the waiting queue is 0, which is equivalent to no capacity. At this time, the only way is If the task is rejected, the rejected operation will be determined based on the rejection policy.

The default rejection policies of the thread pool are as follows:

  • AbortPolicy (default): Like above, throw an exception directly.
  • CallerRunsPolicy: Directly let the thread that submitted the task run the task. For example, if the main thread submits the task to the thread pool, it will be executed directly by the main thread.
  • DiscardOldestPolicy: Discard the latest task in the queue and replace it with the current task.
  • DiscardPolicy: Nothing to do.

Here we conduct a test:

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor =
            new ThreadPoolExecutor(2, 4,
                    3, TimeUnit.SECONDS,
                    new SynchronousQueue<>(),
                    new ThreadPoolExecutor.CallerRunsPolicy());   //使用另一个构造方法,最后一个参数传入策略,比如这里我们使用了CallerRunsPolicy策略

Whoever submits the CallerRunsPolicy policy will execute it himself, so:

pool-1-thread-1 开始执行!(0
pool-1-thread-2 开始执行!(1
main 开始执行!(4
pool-1-thread-4 开始执行!(3
pool-1-thread-3 开始执行!(2
pool-1-thread-3 已结束!(2
pool-1-thread-2 已结束!(1
pool-1-thread-1 已结束!(0
main 已结束!(4
pool-1-thread-4 已结束!(3
pool-1-thread-1 开始执行!(5
pool-1-thread-1 已结束!(5
线程池中线程数量:4
线程池中线程数量:2

It can be seen that when the queue cannot be filled, the task is run directly on the main thread, and then continues execution after the execution is completed.

Let’s try changing the policy to DiscardOldestPolicy:

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor =
            new ThreadPoolExecutor(2, 4,
                    3, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(1),    //这里设置为ArrayBlockingQueue,长度为1
                    new ThreadPoolExecutor.DiscardOldestPolicy());   

It removes the most recent task in the waiting queue, so you can see that one task is actually abandoned:

pool-1-thread-1 开始执行!(0
pool-1-thread-4 开始执行!(4
pool-1-thread-3 开始执行!(3
pool-1-thread-2 开始执行!(1
pool-1-thread-1 已结束!(0
pool-1-thread-4 已结束!(4
pool-1-thread-1 开始执行!(5
线程池中线程数量:4
pool-1-thread-3 已结束!(3
pool-1-thread-2 已结束!(1
pool-1-thread-1 已结束!(5
线程池中线程数量:2

What’s more interesting is that if you choose a SynchronousQueue with no capacity as the waiting queue, the stack will explode:

pool-1-thread-1 开始执行!(0
pool-1-thread-3 开始执行!(2
pool-1-thread-2 开始执行!(1
pool-1-thread-4 开始执行!(3
Exception in thread "main" java.lang.StackOverflowError
	at java.util.concurrent.SynchronousQueue.offer(SynchronousQueue.java:912)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)	
	...
pool-1-thread-1 已结束!(0
pool-1-thread-2 已结束!(1
pool-1-thread-4 已结束!(3
pool-1-thread-3 已结束!(2

Why is this? Let’s take a look at the source code of this rejection policy:

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    
    
    public DiscardOldestPolicy() {
    
     }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        if (!e.isShutdown()) {
    
    
            e.getQueue().poll();   //会先执行一次出队操作,但是这对于SynchronousQueue来说毫无意义
            e.execute(r);     //这里会再次调用execute方法
        }
    }
}

As you can see, it will first dequeue the waiting queue, but since the SynchronousQueue has no capacity at all, all this operation is meaningless, and then the method will be executed recursively. After entering, it is found that there is no capacity and cannot be inserted, so it executerepeats The above operation will recurse infinitely and eventually the stack will explode.

Of course, in addition to using the four official strategies, we can also use customized strategies:

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor =
            new ThreadPoolExecutor(2, 4,
                    3, TimeUnit.SECONDS,
                    new SynchronousQueue<>(),
                    (r, executor1) -> {
    
       //比如这里我们也来实现一个就在当前线程执行的策略
                        System.out.println("哎呀,线程池和等待队列都满了,你自己耗子尾汁吧");
                        r.run();   //直接运行
                    });

Next let's look at the thread creation factory. We can decide how to create new threads ourselves:

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor =
            new ThreadPoolExecutor(2, 4,
                    3, TimeUnit.SECONDS,
                    new SynchronousQueue<>(),
                    new ThreadFactory() {
    
    
                        int counter = 0;
                        @Override
                        public Thread newThread(Runnable r) {
    
    
                            return new Thread(r, "我的自定义线程-"+counter++);
                        }
                    });

    for (int i = 0; i < 4; i++) {
    
    
        executor.execute(() -> System.out.println(Thread.currentThread().getName()+" 开始执行!"));
    }
}

The Runnable object passed in here is the task we submitted. We can see that we need to return a Thread object, which is actually the process of creating a thread in the thread pool. It is up to us to decide how to create this object and some of its attributes.

Have you ever thought about this situation? If an exception occurs during the running of our task, will it cause the threads in the thread pool to be destroyed?

public static void main(String[] args) throws InterruptedException {
    
    
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1,   //最大容量和核心容量锁定为1
            0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
    executor.execute(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
        throw new RuntimeException("我是异常!");
    });
    TimeUnit.SECONDS.sleep(1);
    executor.execute(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
    });
}

As you can see, in the above example, when the thread pool size is only 1,After an abnormality occurs in the thread operation, a new task is submitted again, and the executing thread is a new thread.

In addition to creating thread pools ourselves, the official also provides a lot of thread pool definitions. We can use Executorstool classes to quickly create thread pools:

public static void main(String[] args) throws InterruptedException {
    
    
    ExecutorService executor = Executors.newFixedThreadPool(2);   //直接创建一个固定容量的线程池
}

You can see its internal implementation as:

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

Here, the maximum number of threads and core threads are directly set to the same, and the waiting time is 0, because it is not needed at all, and an unbounded LinkedBlockingQueue is used as the waiting queue.

Use newSingleThreadExecutor to create a thread pool with only one thread:

public static void main(String[] args) throws InterruptedException {
    
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    //创建一个只有一个线程的线程池
}

The principle is as follows:

public static ExecutorService newSingleThreadExecutor() {
    
    
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

You can see that this is not a ThreadPoolExecutor object created directly, but a layer of FinalizableDelegatedExecutorService. So what is this?

static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    
    
    FinalizableDelegatedExecutorService(ExecutorService executor) {
    
    
        super(executor);
    }
    protected void finalize() {
    
        //在GC时,会执行finalize方法,此方法中会关闭掉线程池,释放资源
        super.shutdown();
    }
}
static class DelegatedExecutorService extends AbstractExecutorService {
    
    
    private final ExecutorService e;    //被委派对象
    DelegatedExecutorService(ExecutorService executor) {
    
     e = executor; }   //实际上所以的操作都是让委派对象执行的,有点像代理
    public void execute(Runnable command) {
    
     e.execute(command); }
    public void shutdown() {
    
     e.shutdown(); }
    public List<Runnable> shutdownNow() {
    
     return e.shutdownNow(); }

Therefore, the difference between the following two ways of writing is:

public static void main(String[] args) throws InterruptedException {
    
    
    ExecutorService executor1 = Executors.newSingleThreadExecutor();
    ExecutorService executor2 = Executors.newFixedThreadPool(1);
}

The former is actually proxied, and we cannot directly modify the relevant properties of the former. Obviously, using the former to create a thread pool with only one thread is more professional and safer (it can prevent properties from being modified).

Finally, let’s look at newCachedThreadPoolthe method:

public static void main(String[] args) throws InterruptedException {
    
    
    ExecutorService executor = Executors.newCachedThreadPool();
    //它是一个会根据需要无限制创建新线程的线程池
}

Let's take a look at its implementation:

public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

As you can see, the number of core threads is 0, which means that all threads are 非核心线程. That is to say, if the thread idle time exceeds 1 second, it will be destroyed. But its maximum capacity is Integer.MAX_VALUE, that is to say, it can grow without limit, so this thing must be used with caution.

Execute tasks with return values

A multi-threaded task can not only be a void task with no return value. For example, we need to execute a task now, but we need to get a result after the task is executed. What should we do at this time?

Here we can use Future, which can return the calculation result of the task. We can use it to obtain the result of the task and whether the task is currently completed:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
    ExecutorService executor = Executors.newSingleThreadExecutor();   //直接用Executors创建,方便就完事了
    Future<String> future = executor.submit(() -> "我是字符串!");     //使用submit提交任务,会返回一个Future对象,注意提交的对象可以是Runable也可以是Callable,这里使用的是Callable能够自定义返回值
    System.out.println(future.get());    //如果任务未完成,get会被阻塞,任务完成返回Callable执行结果返回值
    executor.shutdown();
}

Of course, the result can also be defined at the beginning, and then wait for the Runnable to finish executing before returning:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<String> future = executor.submit(() -> {
    
    
        try {
    
    
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }, "我是字符串!");
    System.out.println(future.get());
    executor.shutdown();
}

You can also pass in a FutureTask object:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    ExecutorService service = Executors.newSingleThreadExecutor();
    FutureTask<String> task = new FutureTask<>(() -> "我是字符串!");
    service.submit(task);
    System.out.println(task.get());
    executor.shutdown();
}

We can also get some status of the current task through the Future object:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<String> future = executor.submit(() -> "都看到这里了,不赏UP主一个一键三连吗?");
    System.out.println(future.get());
    System.out.println("任务是否执行完成:"+future.isDone());
    System.out.println("任务是否被取消:"+future.isCancelled());
    executor.shutdown();
}

Let’s try to cancel the task during task execution:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    

    ExecutorService executor = Executors.newSingleThreadExecutor();
    
    Future<String> future = executor.submit(() -> {
    
    
        TimeUnit.SECONDS.sleep(10);
        return "这次一定!";
    });
    
    TimeUnit.SECONDS.sleep(1);
    
    // 疑问: 这里去取消任务, 线程池中的线程没把异常打印出来,但是却可以使用try-catch捕捉到异常?
    //       submit 底层仍然是执行execute,只不过封装了一层future,所以需要future.get()才能进行异常捕获和处理(即:需要对future.get()使用try-catch包裹起来)
    //       (可参考:线程池中某个线程执行有异常,该如何处理? - https://blog.csdn.net/qq_26437925/article/details/127463372)
    System.out.println(future.cancel(true)); 
    System.out.println(future.isCancelled());
    
    executor.shutdown();
}

Execute scheduled tasks

Since the thread pool is so powerful, can the thread pool perform scheduled tasks? If we need to execute a scheduled task before, we will definitely use Timer and TimerTask, but it will only create one thread to process our scheduled task, cannot implement multi-thread scheduling, and it cannot handle exceptions once an uncaught exception is thrown. It will terminate directly. Obviously we need a more powerful timer.

After JDK5, we can use ScheduledThreadPoolExecutor to submit scheduled tasks, which inherits from ThreadPoolExecutor, and all construction methods must require the maximum thread pool capacity to be Integer.MAX_VALUE, and all use DelayedWorkQueue as the waiting queue.

public ScheduledThreadPoolExecutor(int corePoolSize) {
    
    
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}

public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
    
    
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);
}

public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) {
    
    
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler);
}

public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    
    
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);
}

Let’s test its method. This method can submit a delayed task that will only start after the specified time is reached:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
  	//直接设定核心线程数为1
    ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
    //这里我们计划在3秒后执行
    executor.schedule(() -> System.out.println("HelloWorld!"), 3, TimeUnit.SECONDS);

    executor.shutdown();
}

We can also pass in a Callable object to receive the return value as before:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
  	//这里使用ScheduledFuture
    ScheduledFuture<String> future = executor.schedule(() -> "????", 3, TimeUnit.SECONDS);
    System.out.println("任务剩余等待时间:"+future.getDelay(TimeUnit.MILLISECONDS) / 1000.0 + "s");
    System.out.println("任务执行结果:"+future.get());
    executor.shutdown();
}

You can see schedulethat the method returns a ScheduledFuture object. Like Future, it also supports obtaining the return value, including canceling the task, and also supports obtaining the remaining waiting time.

So what if we want to continuously execute tasks at a certain frequency?

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
    executor.scheduleAtFixedRate(() -> System.out.println("Hello World!"),3, 1, TimeUnit.SECONDS);
  	//三秒钟延迟开始,之后每隔一秒钟执行一次
}

/*
scheduleAtFixedRate 以指定的时间间隔执行1次任务, 如果执行任务的时间大于指定的时间间隔, 那么再任务结束后, 立即执行任务
scheduleWithFixedDelay 任务无论执行多久,都要再任务完成之后再间隔指定时间,然后才开始下一轮
*/

Executors also preset the newScheduledThreadPool method for us to create a thread pool:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
    service.schedule(() -> System.out.println("Hello World!"), 1, TimeUnit.SECONDS);
}

Thread pool implementation principle

We learned about the use of the thread pool earlier, so let's take a look at its detailed implementation process. The structure is a bit complicated, so sit tight and let's get started.

Here we need to first introduce the ctl variable:

//这个变量比较关键,用到了原子AtomicInteger,用于同时保存线程池运行状态和线程数量(使用原子类是为了保证原子性)
//它是通过拆分32个bit位来保存数据的,前3位保存状态,后29位保存工作线程数量(那要是工作线程数量29位装不下不就GG?)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;    //29位,线程数量位
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;   //计算得出最大容量(1左移29位,最大容量为2的29次方-1)

// 所有的运行状态,注意都是只占用前3位,不会占用后29位
// 接收新任务,并等待执行队列中的任务
private static final int RUNNING    = -1 << COUNT_BITS;   //111 | 0000... (后29数量位,下同)
// 不接收新任务,但是依然等待执行队列中的任务
private static final int SHUTDOWN   =  0 << COUNT_BITS;   //000 | 数量位
// 不接收新任务,也不执行队列中的任务,并且还要中断正在执行中的任务
private static final int STOP       =  1 << COUNT_BITS;   //001 | 数量位
// 所有的任务都已结束,线程数量为0,即将完全关闭
private static final int TIDYING    =  2 << COUNT_BITS;   //010 | 数量位
// 完全关闭
private static final int TERMINATED =  3 << COUNT_BITS;   //011 | 数量位

// 封装和解析ctl变量的一些方法
private static int runStateOf(int c)     {
    
     return c & ~CAPACITY; }   //对CAPACITY取反就是后29位全部为0,前三位全部为1,接着与c进行与运算,这样就可以只得到前三位的结果了,所以这里是取运行状态
private static int workerCountOf(int c)  {
    
     return c & CAPACITY; }
//同上,这里是为了得到后29位的结果,所以这里是取线程数量
private static int ctlOf(int rs, int wc) {
    
     return rs | wc; }   
// 比如上面的RUNNING, 0,进行与运算之后:
// 111 | 0000000000000000000000000

Insert image description here

Let's start with the simplest one and see executewhat the thread pool will do after calling the method:

//这个就是我们指定的阻塞队列
private final BlockingQueue<Runnable> workQueue;

//再次提醒,这里没加锁!!该有什么意识不用我说了吧,所以说ctl才会使用原子类。
public void execute(Runnable command) {
    
    
    if (command == null)
        throw new NullPointerException();     //如果任务为null,那执行个寂寞,所以说直接空指针
    int c = ctl.get();      //获取ctl的值,一会要读取信息的
    if (workerCountOf(c) < corePoolSize) {
    
       //判断工作线程数量是否小于核心线程数
        if (addWorker(command, true))    //如果是,那不管三七二十一,直接加新的线程执行,然后返回即可
            return;
        c = ctl.get();    //如果线程添加失败(有可能其他线程也在对线程池进行操作),那就更新一下c的值
    }
    if (isRunning(c) && workQueue.offer(command)) {
    
       //继续判断,如果当前线程池是运行状态,那就尝试向阻塞队列中添加一个新的等待任务
        int recheck = ctl.get();   //再次获取ctl的值
        if (! isRunning(recheck) && remove(command))   //这里是再次确认当前线程池是否关闭,如果添加等待任务后线程池关闭了,那就把刚刚加进去任务的又拿出来
            reject(command);   //然后直接拒绝当前任务的提交(会根据我们的拒绝策略决定如何进行拒绝操作)
        else if (workerCountOf(recheck) == 0)   //如果这个时候线程池依然在运行状态,那么就检查一下当前工作线程数是否为0,如果是那就直接添加新线程执行
            addWorker(null, false);   //添加一个新的非核心线程,但是注意没添加任务
      	//其他情况就啥也不用做了
    }
    else if (!addWorker(command, false))   //这种情况要么就是线程池没有运行,要么就是队列满了,按照我们之前的规则,核心线程数已满且队列已满,那么会直接添加新的非核心线程,但是如果已经添加到最大数量,这里肯定是会失败的
        reject(command);   //确实装不下了,只能拒绝
}

If you feel that the idea is quite clear, let's take a look at addWorkerhow to create and execute tasks. There is another lot of code:

private boolean addWorker(Runnable firstTask, boolean core) {
    
    
  	//这里给最外层循环打了个标签,方便一会的跳转操作
    retry:
    for (;;) {
    
        //无限循环,老套路了,注意这里全程没加锁
        int c = ctl.get();     //获取ctl值
        int rs = runStateOf(c);    //解析当前的运行状态

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&   //判断线程池是否不是处于运行状态
            ! (rs == SHUTDOWN &&   //如果不是运行状态,判断线程是SHUTDOWN状态并、任务不为null、等待队列不为空,只要有其中一者不满足,直接返回false,添加失败
               firstTask == null &&   
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
    
       //内层又一轮无限循环,这个循环是为了将线程计数增加,然后才可以真正地添加一个新的线程
            int wc = workerCountOf(c);    //解析当前的工作线程数量
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))    //判断一下还装得下不,如果装得下,看看是核心线程还是非核心线程,如果是核心线程,不能大于核心线程数的限制,如果是非核心线程,不能大于最大线程数限制
                return false;
            if (compareAndIncrementWorkerCount(c))    //CAS自增线程计数,如果增加成功,任务完成,直接跳出继续
                break retry;    //注意这里要直接跳出最外层循环,所以用到了标签(类似于goto语句)
            c = ctl.get();  // 如果CAS失败,更新一下c的值
            if (runStateOf(c) != rs)    //如果CAS失败的原因是因为线程池状态和一开始的不一样了,那么就重新从外层循环再来一次
                continue retry;    //注意这里要直接从最外层循环继续,所以用到了标签(类似于goto语句)
            // 如果是其他原因导致的CAS失败,那只可能是其他线程同时在自增,所以重新再来一次内层循环
        }
    }

  	//好了,线程计数自增也完了,接着就是添加新的工作线程了
    boolean workerStarted = false;   //工作线程是否已启动
    boolean workerAdded = false;    //工作线程是否已添加
    Worker w = null;     //暂时理解为工作线程,别急,我们之后会解读Worker类
    try {
    
    
        w = new Worker(firstTask);     //创建新的工作线程,传入我们提交的任务
        final Thread t = w.thread;    //拿到工作线程中封装的Thread对象
        if (t != null) {
    
          //如果线程不为null,那就可以安排干活了
            final ReentrantLock mainLock = this.mainLock;      //又是ReentrantLock加锁环节,这里开始就是只有一个线程能进入了
            mainLock.lock();
            try {
    
    
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());    //获取当前线程的运行状态

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
    
        //只有当前线程池是正在运行状态,或是SHUTDOWN状态且firstTask为空,那么就继续
                    if (t.isAlive()) // 检查一下线程是否正在运行状态
                        throw new IllegalThreadStateException();   //如果是那肯定是不能运行我们的任务的
                    workers.add(w);    //直接将新创建的Work丢进 workers 集合中
                    int s = workers.size();   //看看当前workers的大小
                    if (s > largestPoolSize)   //这里是记录线程池运行以来,历史上的最多线程数
                        largestPoolSize = s;
                    workerAdded = true;   //工作线程已添加
                }
            } finally {
    
    
                mainLock.unlock();   //解锁
            }
            if (workerAdded) {
    
    
                t.start();   //启动线程
                workerStarted = true;  //工作线程已启动
            }
        }
    } finally {
    
    
        if (! workerStarted)    //如果线程在上面的启动过程中失败了
            addWorkerFailed(w);    //将w移出workers并将计数器-1,最后如果线程池是终止状态,会尝试加速终止线程池
    }
    return workerStarted;   //返回是否成功
}

Next, let’s look at how the Worker class is implemented. It inherits from AbstractQueuedSynchronizer. After two chapters, we actually encountered AQS again, which means that it itself is a lock:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable {
    
    
    //用来干活的线程
    final Thread thread;
    //要执行的第一个任务,构造时就确定了的
    Runnable firstTask;
    //干活数量计数器,也就是这个线程完成了多少个任务
    volatile long completedTasks;

    Worker(Runnable firstTask) {
    
    
        setState(-1); // 执行Task之前不让中断,将AQS的state设定为-1
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);   //通过预定义或是我们自定义的线程工厂创建线程
    }
  
    public void run() {
    
    
        runWorker(this);   //真正开始干活,包括当前活干完了又要等新的活来,就从这里开始,一会详细介绍
    }

   	//0就是没加锁,1就是已加锁
    protected boolean isHeldExclusively() {
    
    
        return getState() != 0;
    }

    ...
}

Finally, let’s take a look at how a Worker performs tasks:

final void runWorker(Worker w) {
    
    
    Thread wt = Thread.currentThread();   //获取当前线程
    Runnable task = w.firstTask;    //取出要执行的任务
    w.firstTask = null;   //然后把Worker中的任务设定为null
    w.unlock(); // 因为一开始为-1,这里是通过unlock操作将其修改回0,只有state大于等于0才能响应中断
    boolean completedAbruptly = true;
    try {
    
    
      	//只要任务不为null,或是任务为空但是可以从等待队列中取出任务不为空,那么就开始执行这个任务,注意这里是无限循环,也就是说如果当前没有任务了,那么会在getTask方法中卡住,因为要从阻塞队列中等着取任务
        while (task != null || (task = getTask()) != null) {
    
    
            w.lock();    //对当前Worker加锁,这里其实并不是防其他线程,而是在shutdown时保护此任务的运行
            
          //由于线程池在STOP状态及以上会禁止新线程加入并且中断正在进行的线程
            if ((runStateAtLeast(ctl.get(), STOP) ||   //只要线程池是STOP及以上的状态,那肯定是不能开始新任务的
                 (Thread.interrupted() &&    					 //线程是否已经被打上中断标记并且线程一定是STOP及以上
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())   //再次确保线程被没有打上中断标记
                wt.interrupt();     //打中断标记
            try {
    
    
                beforeExecute(wt, task);  //开始之前的准备工作,这里暂时没有实现
                Throwable thrown = null;
                try {
    
    
                    task.run();    //OK,开始执行任务
                } catch (RuntimeException x) {
    
    
                    thrown = x; throw x;
                } catch (Error x) {
    
    
                    thrown = x; throw x;
                } catch (Throwable x) {
    
    
                    thrown = x; throw new Error(x);
                } finally {
    
    
                    afterExecute(task, thrown);    //执行之后的工作,也没实现
                }
            } finally {
    
    
                task = null;    //任务已完成,不需要了
                w.completedTasks++;   //任务完成数++
                w.unlock();    //解锁
            }
        }
        completedAbruptly = false;
    } finally {
    
    
      	//如果能走到这一步,那说明上面的循环肯定是跳出了,也就是说这个Worker可以丢弃了
      	//所以这里会直接将 Worker 从 workers 里删除掉
        processWorkerExit(w, completedAbruptly);
    }
}

So how does it get the task from the blocking queue:

private Runnable getTask() {
    
    
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
    
        //无限循环获取
        int c = ctl.get();   //获取ctl 
        int rs = runStateOf(c);      //解析线程池运行状态

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    
          //判断是不是没有必要再执行等待队列中的任务了,也就是处于关闭线程池的状态了
            decrementWorkerCount();     //直接减少一个工作线程数量
            return null;    //返回null,这样上面的runWorker就直接结束了,下同
        }

        int wc = workerCountOf(c);   //如果线程池运行正常,那就获取当前的工作线程数量

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;   //如果线程数大于核心线程数或是允许核心线程等待超时,那么就标记为可超时的

      	//超时或maximumPoolSize在运行期间被修改了,并且线程数大于1或等待队列为空,那也是不能获取到任务的
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
    
    
            if (compareAndDecrementWorkerCount(c))   //如果CAS减少工作线程成功
                return null;    //返回null
            continue;   //否则开下一轮循环
        }

        try {
    
    
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :   //如果可超时,那么最多等到超时时间
                workQueue.take();    //如果不可超时,那就一直等着拿任务
            if (r != null)    //如果成功拿到任务,ok,返回
                return r;
            timedOut = true;   //否则就是超时了,下一轮循环将直接返回null
        } catch (InterruptedException retry) {
    
    
            timedOut = false;
        }
      	//开下一轮循环吧
    }
}

Although our source code interpretation is getting deeper and deeper, as long as you keep thinking, you can still continue reading. At this point, the source code interpretation of the relevant execute()methods starts here.

Next let's see what happens when the thread pool is closed:

//普通的shutdown会继续将等待队列中的线程执行完成后再关闭线程池
public void shutdown() {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
      	//判断是否有权限终止
        checkShutdownAccess();
      	//CAS将线程池运行状态改为SHUTDOWN状态,还算比较温柔,详细过程看下面
        advanceRunState(SHUTDOWN);
       	//让闲着的线程(比如正在等新的任务)中断,但是并不会影响正在运行的线程,详细过程请看下面
        interruptIdleWorkers();
        onShutdown(); //给ScheduledThreadPoolExecutor提供的钩子方法,就是等ScheduledThreadPoolExecutor去实现的,当前类没有实现
    } finally {
    
    
        mainLock.unlock();
    }
    tryTerminate();   //最后尝试终止线程池
}
private void advanceRunState(int targetState) {
    
    
    for (;;) {
    
    
        int c = ctl.get();    //获取ctl
        if (runStateAtLeast(c, targetState) ||    //是否大于等于指定的状态
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))   //CAS设置ctl的值
            break;   //任意一个条件OK就可以结束了
    }
}
private void interruptIdleWorkers(boolean onlyOne) {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        for (Worker w : workers) {
    
    
            Thread t = w.thread;    //拿到Worker中的线程
            if (!t.isInterrupted() && w.tryLock()) {
    
       //先判断一下线程是不是没有被中断然后尝试加锁,但是通过前面的runWorker()源代码我们得知,开始之后是让Worker加了锁的,所以如果线程还在执行任务,那么这里肯定会false
                try {
    
    
                    t.interrupt();    //如果走到这里,那么说明线程肯定是一个闲着的线程,直接给中断吧
                } catch (SecurityException ignore) {
    
    
                } finally {
    
    
                    w.unlock();    //解锁
                }
            }
            if (onlyOne)   //如果只针对一个Worker,那么就结束循环
                break;
        }
    } finally {
    
    
        mainLock.unlock();
    }
}

The method is similar shutdownNow(), but here it will be more direct:

//shutdownNow开始后,不仅不允许新的任务到来,也不会再执行等待队列的线程,而且会终止正在执行的线程
public List<Runnable> shutdownNow() {
    
    
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        checkShutdownAccess();
      	//这里就是直接设定为STOP状态了,不再像shutdown那么温柔
        advanceRunState(STOP);
      	//直接中断所有工作线程,详细过程看下面
        interruptWorkers();
      	//取出仍处于阻塞队列中的线程
        tasks = drainQueue();
    } finally {
    
    
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;   //最后返回还没开始的任务
}
private void interruptWorkers() {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        for (Worker w : workers)   //遍历所有Worker
            w.interruptIfStarted();   //无差别对待,一律加中断标记
    } finally {
    
    
        mainLock.unlock();
    }
}

Finally, let’s take a look at tryTerminate()how to completely terminate a thread pool:

final void tryTerminate() {
    
    
    for (;;) {
    
         //无限循环
        int c = ctl.get();    //上来先获取一下ctl值
      	//只要是正在运行 或是 线程池基本上关闭了 或是 处于SHUTDOWN状态且工作队列不为空,那么这时还不能关闭线程池,返回
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
      
      	//走到这里,要么处于SHUTDOWN状态且等待队列为空或是STOP状态
        if (workerCountOf(c) != 0) {
    
     // 如果工作线程数不是0,这里也会中断空闲状态下的线程
            interruptIdleWorkers(ONLY_ONE);   //这里最多只中断一个空闲线程,然后返回
            return;
        }

      	//走到这里,工作线程也为空了,可以终止线程池了
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
    
       //先CAS将状态设定为TIDYING表示基本终止,正在做最后的操作
                try {
    
    
                    terminated();   //终止,暂时没有实现
                } finally {
    
    
                    ctl.set(ctlOf(TERMINATED, 0));   //最后将状态设定为TERMINATED,线程池结束了它年轻的生命
                    termination.signalAll();    //如果有线程调用了awaitTermination方法,会等待当前线程池终止,到这里差不多就可以唤醒了
                }
                return;   //结束
            }
          	//注意如果CAS失败会直接进下一轮循环重新判断
        } finally {
    
    
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

OK, regarding the implementation principle of the thread pool, we will temporarily introduce it here. Regarding the more advanced scheduled task thread pool, we will not explain it here.


Concurrency tools

Counter lockCountDownLatch

Multitasking synchronization artifact. It allows one or more threads to wait for other threads to complete their work. For example, now we have such a requirement:

  • There are 20 computing tasks. We need to calculate all the results of these tasks first. The execution time of each task is unknown.
  • When all tasks are completed, the final results will be consolidated immediately.

To realize this requirement, there is a very troublesome point. We don't know when the task will be completed. So can the final statistics be delayed for a certain period of time? However, no matter how long the final statistics are delayed, there is either no guarantee that all tasks have been completed, or it is possible that all tasks have been completed and are still waiting here.

Therefore, we need a tool that can synchronize subtasks.

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch latch = new CountDownLatch(20);  //创建一个初始值为10的计数器锁
    for (int i = 0; i < 20; i++) {
    
    
        int finalI = i;
        new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep((long) (2000 * new Random().nextDouble()));
                System.out.println("子任务"+ finalI +"执行完成!");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            latch.countDown();   //每执行一次计数器都会-1
        }).start();
    }

    //开始等待所有的线程完成,当计数器为0时,恢复运行
    latch.await();   //这个操作可以同时被多个线程执行,一起等待,这里只演示了一个
    System.out.println("所有子任务都完成!任务完成!!!");
  
  	//注意这个计数器只能使用一次,用完只能重新创一个,没有重置的说法
}

After we call await()the method, it is actually a process of waiting for the counter to decay to 0, and the self-decrement operation is completed by each sub-thread. When the sub-thread completes the work, then the counter is -1, and all sub-threads are completed. After that, the counter reaches 0 and the wait ends.

So how is it achieved? The implementation principle is very simple:

public class CountDownLatch {
    
    
   	//同样是通过内部类实现AbstractQueuedSynchronizer
    private static final class Sync extends AbstractQueuedSynchronizer {
    
    
        
        Sync(int count) {
    
       //这里直接使用AQS的state作为计数器(可见state能被玩出各种花样),也就是说一开始就加了count把共享锁,当线程调用countdown时,就解一层锁
            setState(count);
        }

        int getCount() {
    
    
            return getState();
        }

      	//采用共享锁机制,因为可以被不同的线程countdown,所以实现的tryAcquireShared和tryReleaseShared
      	//获取这把共享锁其实就是去等待state被其他线程减到0
        protected int tryAcquireShared(int acquires) {
    
    
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
    
    
            // 每次执行都会将state值-1,直到为0
            for (;;) {
    
    
                int c = getState();
                if (c == 0)
                    return false;   //如果已经是0了,那就false
                int nextc = c-1;
                if (compareAndSetState(c, nextc))   //CAS设置state值,失败直接下一轮循环
                    return nextc == 0;    //返回c-1之后,是不是0,如果是那就true,否则false,也就是说只有刚好减到0的时候才会返回true
            }
        }
    }

    private final Sync sync;

    public CountDownLatch(int count) {
    
    
        if (count < 0) throw new IllegalArgumentException("count < 0");  //count那肯定不能小于0啊
        this.sync = new Sync(count);   //构造Sync对象,将count作为state初始值
    }

   	//通过acquireSharedInterruptibly方法获取共享锁,但是如果state不为0,那么会被持续阻塞,详细原理下面讲
    public void await() throws InterruptedException {
    
    
        sync.acquireSharedInterruptibly(1);
    }

    //同上,但是会超时
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

   	//countDown其实就是解锁一次
    public void countDown() {
    
    
        sync.releaseShared(1);
    }

    //获取当前的计数,也就是AQS中state的值
    public long getCount() {
    
    
        return sync.getCount();
    }

    //这个就不说了
    public String toString() {
    
    
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

Before we go into the in-depth explanation, let us first have a general understanding of the basic implementation ideas of CountDownLatch:

  • Implemented using shared locks
  • At the beginning, the count layer lock was already in the state, that is,state = count
  • await()That is to add a shared lock, but it must be statefor 0the lock to be successful, otherwise according to the AQS mechanism, it will enter the waiting queue and block, and the blocking will end after the lock is successful.
  • countDown()It is to unlock 1the layer lock, that is, rely on this method stateto reduce the value bit by bit to0

Since we only explained exclusive locks before and did not explain shared locks, we will mention it a little here:

public final void acquireShared(int arg) {
    
    
    if (tryAcquireShared(arg) < 0)   //上来就调用tryAcquireShared尝试以共享模式获取锁,小于0则失败,上面判断的是state==0返回1,否则-1,也就是说如果计数器不为0,那么这里会判断成功
        doAcquireShared(arg);   //计数器不为0的时候,按照它的机制,那么会阻塞,所以我们来看看doAcquireShared中是怎么进行阻塞的
}
private void doAcquireShared(int arg) {
    
    
    final Node node = addWaiter(Node.SHARED);   //向等待队列中添加一个新的共享模式结点
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        for (;;) {
    
        //无限循环
            final Node p = node.predecessor();   //获取当前节点的前驱的结点
            if (p == head) {
    
        //如果p就是头结点,那么说明当前结点就是第一个等待节点
                int r = tryAcquireShared(arg);    //会再次尝试获取共享锁
                if (r >= 0) {
    
          //要是获取成功
                    setHeadAndPropagate(node, r);   //那么就将当前节点设定为新的头结点,并且会继续唤醒后继节点
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&   //和独占模式下一样的操作,这里不多说了
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);   //如果最后都还是没获取到,那么就cancel
    }
}
//其实感觉大体上和独占模式的获取有点像,但是它多了个传播机制,会继续唤醒后续节点
private void setHeadAndPropagate(Node node, int propagate) {
    
    
    Node h = head; // 取出头结点并将当前节点设定为新的头结点
    setHead(node);
    
  	//因为一个线程成功获取到共享锁之后,有可能剩下的等待中的节点也有机会拿到共享锁
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
    
       //如果propagate大于0(表示共享锁还能继续获取)或是h.waitStatus < 0,这是由于在其他线程释放共享锁时,doReleaseShared会将状态设定为PROPAGATE表示可以传播唤醒,后面会讲
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();   //继续唤醒下一个等待节点
    }
}

Let's take a look at its countdown process:

public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
       //直接尝试释放锁,如果成功返回true(在CountDownLatch中只有state减到0的那一次,会返回true)
        doReleaseShared();    //这里也会调用doReleaseShared继续唤醒后面的结点
        return true;
    }
    return false;   //其他情况false
  									//不过这里countdown并没有用到这些返回值
}
private void doReleaseShared() {
    
    
    for (;;) {
    
       //无限循环
        Node h = head;    //获取头结点
        if (h != null && h != tail) {
    
        //如果头结点不为空且头结点不是尾结点,那么说明等待队列中存在节点
            int ws = h.waitStatus;    //取一下头结点的等待状态
            if (ws == Node.SIGNAL) {
    
        //如果是SIGNAL,那么就CAS将头结点的状态设定为初始值
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            //失败就开下一轮循环重来
                unparkSuccessor(h);    //和独占模式一样,当锁被释放,都会唤醒头结点的后继节点,doAcquireShared循环继续,如果成功,那么根据setHeadAndPropagate,又会继续调用当前方法,不断地传播下去,让后面的线程一个一个地获取到共享锁,直到不能再继续获取为止
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))   //如果等待状态是默认值0,那么说明后继节点已经被唤醒,直接将状态设定为PROPAGATE,它代表在后续获取资源的时候,够向后面传播
                continue;                //失败就开下一轮循环重来
        }
        if (h == head)                   // 如果头结点发生了变化,不会break,而是继续循环,否则直接break退出
            break;
    }
}

Maybe you are still a little confused after reading this, let’s sort it out again:

  • Shared locks are shared by threads, and multiple threads can own shared locks at the same time.
  • If a thread has just acquired a shared lock, then the thread waiting after it is very likely to be able to acquire the lock, so it has to be propagated and continue to try to wake up subsequent nodes. Unlike exclusive locks, exclusive locks do not need to consider these at all.
  • If a thread has just released a lock, whether it is an exclusive lock or a shared lock, it needs to wake up subsequent threads waiting for the node.

Go back to CountDownLatch, and then combine it with the entire AQS shared lock implementation mechanism to conduct a complete derivation. It is relatively simple to understand.

CyclicBarrier

Just like a game, we must wait until there are enough people in the room before it can start, and after the game starts, players need to enter the game at the same time to ensure fairness.

If there are currently 5 people in the game room, but 10 people are required to start the game, so we must wait for the remaining 5 people to arrive before starting the game, and ensure that all players enter at the same time when the game starts, then how to implement this function? We can use CyclicBarrier, which translates as cyclic barrier, so this barrier officially appears to solve this problem.

public static void main(String[] args) {
    
    
    CyclicBarrier barrier = new CyclicBarrier(10,   //创建一个初始值为10的循环屏障
                () -> System.out.println("飞机马上就要起飞了,各位特种兵请准备!"));   //人等够之后执行的任务
    for (int i = 0; i < 10; i++) {
    
    
        int finalI = i;
        new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep((long) (2000 * new Random().nextDouble()));
                System.out.println("玩家 "+ finalI +" 进入房间进行等待... ("+barrier.getNumberWaiting()+"/10)");

                barrier.await();    //调用await方法进行等待,直到等待的线程足够多为止

                //开始游戏,所有玩家一起进入游戏
                System.out.println("玩家 "+ finalI +" 进入游戏!");
            } catch (InterruptedException | BrokenBarrierException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    }
}

可以看到,循环屏障会不断阻挡线程,直到被阻挡的线程足够多时,才能一起冲破屏障,并且在冲破屏障时,我们也可以做一些其他的任务。这和人多力量大的道理是差不多的,当人足够多时方能冲破阻碍,到达美好的明天。当然,屏障由于是可循环的,所以它在被冲破后,会重新开始计数,继续阻挡后续的线程:

public static void main(String[] args) {
    
    
    CyclicBarrier barrier = new CyclicBarrier(5);  //创建一个初始值为5的循环屏障

    for (int i = 0; i < 10; i++) {
    
       //创建5个线程
        int finalI = i;
        new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep((long) (2000 * new Random().nextDouble()));
                System.out.println("玩家 "+ finalI +" 进入房间进行等待... ("+barrier.getNumberWaiting()+"/5)");

                barrier.await();    //调用await方法进行等待,直到等待线程到达5才会一起继续执行

                //人数到齐之后,可以开始游戏了
                System.out.println("玩家 "+ finalI +" 进入游戏!");
            } catch (InterruptedException | BrokenBarrierException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    }
}

可以看到,通过使用循环屏障,我们可以对线程进行一波一波地放行,每一波都放行5个线程,当然除了自动重置之外,我们也可以调用reset()方法来手动进行重置操作,同样会重新计数:

public static void main(String[] args) throws InterruptedException {
    
    
    CyclicBarrier barrier = new CyclicBarrier(5);  //创建一个初始值为10的计数器锁

    for (int i = 0; i < 3; i++)
        new Thread(() -> {
    
    
            try {
    
    
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
    
    
                e.printStackTrace();
            }
        }).start();

    Thread.sleep(500);   //等一下上面的线程开始运行
    System.out.println("当前屏障前的等待线程数:"+barrier.getNumberWaiting());

    barrier.reset();
    System.out.println("重置后屏障前的等待线程数:"+barrier.getNumberWaiting());
}

可以看到,在调用reset()之后,处于等待状态下的线程,全部被中断并且抛出BrokenBarrierException异常,循环屏障等待线程数归零。那么要是处于等待状态下的线程被中断了呢?屏障的线程等待数量会不会自动减少?

public static void main(String[] args) throws InterruptedException {
    
    
    CyclicBarrier barrier = new CyclicBarrier(10);
    Runnable r = () -> {
    
    
        try {
    
    
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
    
    
            e.printStackTrace();
        }
    };
    Thread t = new Thread(r);
    t.start();
    t.interrupt();
    new Thread(r).start();
}

可以看到,当await()状态下的线程被中断,那么屏障会直接变成损坏状态,一旦屏障损坏,那么这一轮就无法再做任何等待操作了。也就是说,本来大家计划一起合力冲破屏障,结果有一个人摆烂中途退出了,那么所有人的努力都前功尽弃,这一轮的屏障也不可能再被冲破了(所以CyclicBarrier告诉我们,不要做那个害群之马,要相信你的团队,不然没有好果汁吃),只能进行reset()重置操作进行重置才能恢复正常。

乍一看,怎么感觉和之前讲的CountDownLatch有点像,好了,这里就得区分一下了,千万别搞混:

  • CountDownLatch:
    1. 它只能使用一次,是一个一次性的工具
    2. 它是一个或多个线程用于等待其他线程完成的同步工具
  • CyclicBarrier
    1. 它可以反复使用,允许自动或手动重置计数
    2. 它是让一定数量的线程在同一时间开始运行的同步工具

我们接着来看循环屏障的实现细节:

public class CyclicBarrier {
    
    
    //内部类,存放broken标记,表示屏障是否损坏,损坏的屏障是无法正常工作的
    private static class Generation {
    
    
        boolean broken = false;
    }

    /** 内部维护一个可重入锁 */
    private final ReentrantLock lock = new ReentrantLock();
    /** 再维护一个Condition */
    private final Condition trip = lock.newCondition();
    /** 这个就是屏障的最大阻挡容量,就是构造方法传入的初始值 */
    private final int parties;
    /* 在屏障破裂时做的事情 */
    private final Runnable barrierCommand;
    /** 当前这一轮的Generation对象,每一轮都有一个新的,用于保存broken标记 */
    private Generation generation = new Generation();

    //默认为最大阻挡容量,每来一个线程-1,和CountDownLatch挺像,当屏障破裂或是被重置时,都会将其重置为最大阻挡容量
    private int count;

  	//构造方法
  	public CyclicBarrier(int parties, Runnable barrierAction) {
    
    
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
  
    public CyclicBarrier(int parties) {
    
    
        this(parties, null);
    }
  
    //开启下一轮屏障,一般屏障被冲破之后,就自动重置了,进入到下一轮
    private void nextGeneration() {
    
    
        // 唤醒所有等待状态的线程
        trip.signalAll();
        // 重置count的值
        count = parties;
      	//创建新的Generation对象
        generation = new Generation();
    }

    //破坏当前屏障,变为损坏状态,之后就不能再使用了,除非重置
    private void breakBarrier() {
    
    
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }
  
  	//开始等待
  	public int await() throws InterruptedException, BrokenBarrierException {
    
    
        try {
    
    
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
    
    
            throw new Error(toe); // 因为这里没有使用定时机制,不可能发生异常,如果发生怕是出了错误
        }
    }
    
  	//可超时的等待
    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
    
    
        return dowait(true, unit.toNanos(timeout));
    }

    //这里就是真正的等待流程了,让我们细细道来
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();   //加锁,注意,因为多个线程都会调用await方法,因此只有一个线程能进,其他都被卡着了
        try {
    
    
            final Generation g = generation;   //获取当前这一轮屏障的Generation对象

            if (g.broken)
                throw new BrokenBarrierException();   //如果这一轮屏障已经损坏,那就没办法使用了

            if (Thread.interrupted()) {
    
       //如果当前等待状态的线程被中断,那么会直接破坏掉屏障,并抛出中断异常(破坏屏障的第1种情况)
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;     //如果上面都没有出现不正常,那么就走正常流程,首先count自减并赋值给index,index表示当前是等待的第几个线程
            if (index == 0) {
    
      // 如果自减之后就是0了,那么说明来的线程已经足够,可以冲破屏障了
                boolean ranAction = false;
                try {
    
    
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();   //执行冲破屏障后的任务,如果这里抛异常了,那么会进finally
                    ranAction = true;
                    nextGeneration();   //一切正常,开启下一轮屏障(方法进入之后会唤醒所有等待的线程,这样所有的线程都可以同时继续运行了)然后返回0,注意最下面finally中会解锁,不然其他线程唤醒了也拿不到锁啊
                    return 0;
                } finally {
    
    
                    if (!ranAction)   //如果是上面出现异常进来的,那么也会直接破坏屏障(破坏屏障的第2种情况)
                        breakBarrier();
                }
            }

            // 能走到这里,那么说明当前等待的线程数还不够多,不足以冲破屏障
            for (;;) {
    
       //无限循环,一直等,等到能冲破屏障或是出现异常为止
                try {
    
    
                    if (!timed)
                        trip.await();    //如果不是定时的,那么就直接永久等待
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);   //否则最多等一段时间
                } catch (InterruptedException ie) {
    
        //等的时候会判断是否被中断(依然是破坏屏障的第1种情况)
                    if (g == generation && ! g.broken) {
    
    
                        breakBarrier();
                        throw ie;
                    } else {
    
    
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();   //如果线程被唤醒之后发现屏障已经被破坏,那么直接抛异常

                if (g != generation)   //成功冲破屏障开启下一轮,那么直接返回当前是第几个等待的线程。
                    return index;

                if (timed && nanos <= 0L) {
    
       //线程等待超时,也会破坏屏障(破坏屏障的第3种情况)然后抛异常
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
    
    
            lock.unlock();    //最后别忘了解锁,不然其他线程拿不到锁
        }
    }

  	//不多说了
    public int getParties() {
    
    
        return parties;
    }

  	//判断是否被破坏,也是加锁访问,因为有可能这时有其他线程正在执行dowait
    public boolean isBroken() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            return generation.broken;
        } finally {
    
    
            lock.unlock();
        }
    }

  	//重置操作,也要加锁
    public void reset() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            breakBarrier();   // 先破坏这一轮的线程,注意这个方法会先破坏再唤醒所有等待的线程,那么所有等待的线程会直接抛BrokenBarrierException异常(详情请看上方dowait倒数第13行)
            nextGeneration(); // 开启下一轮
        } finally {
    
    
            lock.unlock();
        }
    }
	
  	//获取等待线程数量,也要加锁
    public int getNumberWaiting() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            return parties - count;   //最大容量 - 当前剩余容量 = 正在等待线程数
        } finally {
    
    
            lock.unlock();
        }
    }
}

看完了CyclicBarrier的源码之后,是不是感觉比CountDownLatch更简单一些?

信号量 Semaphore

Remember the semaphore mechanism we learned in "Operating System"? It plays a very big role in solving synchronization problems between processes.

Semaphore, sometimes called a semaphore, is a facility used in a multi-threaded environment to ensure that two or more critical code segments are not called concurrently. Before entering a critical code section, the thread must obtain a semaphore; once the critical code section is completed, the thread must release the semaphore. Other threads that want to enter this critical code section must wait until the first thread releases the semaphore.

By using semaphores, we can determine the maximum number of threads that can access a resource at the same time, which is equivalent to flow control of access to a resource. Simply put, it is an exclusive lock that can be occupied by N threads (therefore also supporting fair and unfair modes). We can set the number of Semaphore licenses at the beginning, and each thread can obtain 1 or n licenses, when the licenses are exhausted or insufficient for other threads to obtain, other threads will be blocked.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    //每一个Semaphore都会在一开始获得指定的许可证数数量,也就是许可证配额
    Semaphore semaphore = new Semaphore(2);   //许可证配额设定为2

    for (int i = 0; i < 3; i++) {
    
    
        new Thread(() -> {
    
    
            try {
    
    
                semaphore.acquire();   //申请一个许可证
                System.out.println("许可证申请成功!");
                semaphore.release();   //归还一个许可证
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    //每一个Semaphore都会在一开始获得指定的许可证数数量,也就是许可证配额
    Semaphore semaphore = new Semaphore(3);   //许可证配额设定为3

    for (int i = 0; i < 2; i++)
        new Thread(() -> {
    
    
            try {
    
    
                semaphore.acquire(2);    //一次性申请两个许可证
                System.out.println("许可证申请成功!");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    
}

We can also get some general information through Semaphore:

public static void main(String[] args) throws InterruptedException {
    
    
    Semaphore semaphore = new Semaphore(3);   //只配置一个许可证,5个线程进行争抢,不内卷还想要许可证?
    for (int i = 0; i < 5; i++)
        new Thread(semaphore::acquireUninterruptibly).start();   //可以以不响应中断(主要是能简写一行,方便)
    Thread.sleep(500);
    System.out.println("剩余许可证数量:"+semaphore.availablePermits());
    System.out.println("是否存在线程等待许可证:"+(semaphore.hasQueuedThreads() ? "是" : "否"));
    System.out.println("等待许可证线程数量:"+semaphore.getQueueLength());
}

We can manually recycle all licenses:

public static void main(String[] args) throws InterruptedException {
    
    
    Semaphore semaphore = new Semaphore(3);
    new Thread(semaphore::acquireUninterruptibly).start();
    Thread.sleep(500);
    System.out.println("收回剩余许可数量:"+semaphore.drainPermits());   //直接回收掉剩余的许可证
}

Let's simulate it here. For example, there are 10 threads performing tasks at the same time. The task requirement is to execute a certain method, but this method can only be executed by up to 5 threads at the same time. It is very suitable for us to use a semaphore here.

Data ExchangeExchanger

Data transfer between threads can also be as simple as this.

Using Exchanger, it can realize data exchange between threads:

public static void main(String[] args) throws InterruptedException {
    
    
    Exchanger<String> exchanger = new Exchanger<>();
    new Thread(() -> {
    
    
        try {
    
    
            System.out.println("收到主线程传递的交换数据:"+exchanger.exchange("AAAA"));
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }).start();
    System.out.println("收到子线程传递的交换数据:"+exchanger.exchange("BBBB"));
}

After calling exchangethe method, the current thread will wait for other threads to call the method of the same exchanger object exchange. When another thread also calls the method, the method will return the parameters passed in by the other thread.

It can be seen that the function is relatively simple.

Fork/Join framework

In JDK7, a new framework emerged for parallel execution of tasks. Its purpose is to split large tasks into multiple small tasks, and finally aggregate the results of multiple small tasks to obtain the results of the entire large task, and these Small tasks are all performed at the same time, greatly improving computing efficiency. Fork means splitting, and Join means merging.

Let's demonstrate the actual situation. For example, a calculation formula: 18x7+36x8+9x77+8x53 can be divided into four small tasks: 18x7, 36x8, 9x77, 8x53. Finally, we only need to add up the results of these four tasks. , which is the result of our original calculation, a bit like merge sorting.

Insert image description here

It doesn't just split tasks and use multiple threads, but it can also use work-stealing algorithms to improve thread utilization.

**Work stealing algorithm:** refers to a thread stealing tasks from other queues for execution. A large task is divided into several subtasks that are independent of each other. In order to reduce competition between threads, these subtasks are placed in different queues, and a separate thread is created for each queue to execute the tasks in the queue. Threads and queues correspond one to one. However, some threads will finish the tasks in their own queue first, while other threads still have tasks to be processed in the queues corresponding to them. Instead of waiting, the thread that has finished its work might as well help other threads to work, so it goes to the queue of other threads to steal a task for execution.

Insert image description here

Now let's take a look at how to use it. Here we take the calculation of the sum of 1-1000 as an example. We can split it into 8 small segments and add them together, such as 1-125, 126-250... and finally summarize them. , it also relies on the thread pool to achieve:

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        ForkJoinPool pool = new ForkJoinPool();
        System.out.println(pool.submit(new SubTask(1, 1000)).get());
    }


  	//继承RecursiveTask,这样才可以作为一个任务,泛型就是计算结果类型
    private static class SubTask extends RecursiveTask<Integer> {
    
    
        private final int start;   //比如我们要计算一个范围内所有数的和,那么就需要限定一下范围,这里用了两个int存放
        private final int end;

        public SubTask(int start, int end) {
    
    
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
    
    
            if(end - start > 125) {
    
        //每个任务最多计算125个数的和,如果大于继续拆分,小于就可以开始算了
                SubTask subTask1 = new SubTask(start, (end + start) / 2);
                subTask1.fork();    //会继续划分子任务执行
                SubTask subTask2 = new SubTask((end + start) / 2 + 1, end);
                subTask2.fork();   //会继续划分子任务执行
                return subTask1.join() + subTask2.join();   //越玩越有递归那味了
            } else {
    
    
                System.out.println(Thread.currentThread().getName()+" 开始计算 "+start+"-"+end+" 的值!");
                int res = 0;
                for (int i = start; i <= end; i++) {
    
    
                    res += i;
                }
                return res;   //返回的结果会作为join的结果
            }
        }
    }
}
ForkJoinPool-1-worker-2 开始计算 1-125 的值!
ForkJoinPool-1-worker-2 开始计算 126-250 的值!
ForkJoinPool-1-worker-0 开始计算 376-500 的值!
ForkJoinPool-1-worker-6 开始计算 751-875 的值!
ForkJoinPool-1-worker-3 开始计算 626-750 的值!
ForkJoinPool-1-worker-5 开始计算 501-625 的值!
ForkJoinPool-1-worker-4 开始计算 251-375 的值!
ForkJoinPool-1-worker-7 开始计算 876-1000 的值!
500500

As you can see, the results are very correct, but the entire computing task is actually split into 8 subtasks that are completed simultaneously. Combined with multi-threading, the original single-threaded task is doubled in speed with the support of multi-threading.

The parallel sorting provided by the Arrays tool class is also implemented using ForkJoinPool:

public static void parallelSort(byte[] a) {
    
    
    int n = a.length, p, g;
    if (n <= MIN_ARRAY_SORT_GRAN ||
        (p = ForkJoinPool.getCommonPoolParallelism()) == 1)
        DualPivotQuicksort.sort(a, 0, n - 1);
    else
        new ArraysParallelSortHelpers.FJByte.Sorter
            (null, a, new byte[n], 0, n, 0,
             ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
             MIN_ARRAY_SORT_GRAN : g).invoke();
}

The performance of parallel sorting is definitely better than ordinary sorting in a multi-core CPU environment, and the larger the sorting scale, the more significant the advantage.

At this point, the concurrent programming chapter is complete.

Guess you like

Origin blog.csdn.net/qq_16992475/article/details/132796613