其他分享
首页 > 其他分享> > 哈啰--AQS

哈啰--AQS

作者:互联网

目录

AQS

关于多线程,不论是面试还是工作当中,它绝对算的上是重灾区,而我们使用多线程的目的说白了就是想要发挥多处理器的作用,提高程序性能和响应速度。

然而在多线程环境下,会存在一定的线程安全问题,为了保证高并发场景下的线程安全,绕不开的就是同步与锁机制,但它实现起来非常复杂且容易出现问题。

这个时候有位神人看不下去了,它鼻梁挂着眼镜,留着一嘴的白胡子,脸上挂着看起来有点腼腆的笑容,如果Java的历史,是以人为主体串接起来的话,那么一定少不了这个老头,他就是Doug Lea,我们平时开发过程中使用的JUC(java.util.concurrent )并发包,就是由他老人家主导设计的,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,大大的提高了 Java 并发编程的易用性。

好用归好用,我们大多数应该是不满足于做一个API调用的程序员,所以带着好奇心,我们需要了解一下它底层的实现原理。

AQS核心思想

在分析JUC的时候,绕不开的一个类就是 AbstractQueuedSynchronizer这个抽象类,我们简称为AQS框架,它为不同场景提供了实现锁及同步方式,为同步状态的原子性管理、线程的阻塞、线程的解除阻塞及排队管理提供了一套通用的机制,是实现 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等类的基础。

既然可以看作是个框架,那么它一定有它的设计思想在里面,AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,同时将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

那么它是怎么实现的呢?

AQS主要就是维护了一个volatile int型的state属性来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改,详细说明如下:

在这里插入图片描述

AQS资源共享方式分为:

AQS能干嘛

我们刚才说ReentrantLock 、CountDownLatch、ReentrantReadWriteLock 、Semaphore 等等的实现都和AQS有关,源码为证,如图所示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

锁的实现方式

在分析AQS源码之前,我们先来回忆一下,关于锁的实现方式和线程间的通信方式。

我们常用锁的实现方式有两种,一种是使用synchronized同步代码块实现,另一种就是用JUC并发包中的Lock实现,二者都支持可重入,也就是我们经常听到的可重入锁。

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞,其优点是可一定程度避免死锁,下面我们通过代码来验证一下,

测试代码:

public class LockTest {
    static Object object=new Object();

    static  Lock lock=new ReentrantLock();

    static void printf(){
        synchronized (object){
            System.out.println(Thread.currentThread().getName()+"进入了synchronized  1");
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"进入了synchronized  2");
                synchronized (object){
                    System.out.println(Thread.currentThread().getName()+"进入了synchronized  3");

                }

            }
        }
    }

    static  void printf2(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"进入了 lock  11");
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"进入了 lock  22");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+"进入了 lock  33");

                }finally {
                    lock.unlock();
                }
            }finally {
                lock.unlock();
            }
        }finally {
            lock.unlock();//lock 和unlock要匹配;如果把该行注释的话,那么第一个线程正常运行没有问题,
                         // 第二个线程会迟迟获取不到锁
        }
    }

    public static void main(String[] args) {

        new Thread(() -> printf(),"线程 1").start();
        new Thread(() -> printf(),"线程 2").start();

        new Thread(() -> printf2(),"线程 1").start();
        new Thread(() -> printf2(),"线程 2").start();





    }
}

打印结果 :

在这里插入图片描述

线程的通信方式

回忆过锁的两种实现方式后,我们再来回顾一下线程间的通信方式,也就是让线程等待和唤醒线程的方式,基于刚才提到过的两种实现锁的方式,它们各自实现线程间通信的方式也不同,我们分别来看下:

1、wait/notify

使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程,但是在使用时需要注意以下两个问题:

直接上代码说明:

public class WaitAndNotifyTest {
    static  Object lockObject =new Object();
    public static void main(String[] args) {
        new Thread(() -> {
           // try {                         
            //    Thread.sleep(3000);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
           // }
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName()+"进来了");
                try {
                    lockObject.wait();
                    System.out.println(Thread.currentThread().getName()+"被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName()+"唤醒");
                    lockObject.notify();
            }
        },"B").start();
    }
}

上面的例子很简单,两个线程,A调用wait(),然后B调用notify()唤醒,正常运行下没有问题,打印结果如下

在这里插入图片描述

1.1、要先wait后notify

如果我们把线程A中的sleep注释去掉的话,即B线程先notify,然后A再wait,此时就会出现A一直wait而没有人唤醒导致程序无法退出的问题,如图

在这里插入图片描述

1.2、必须在synchronized中使用

再一个,wait和notify需要在synchronized代码块中使用,如果把synchronized部分注释掉的话,即直接使用wait和notify时,程序运行会报错,如图

在这里插入图片描述

第15行和第26行对应代码分别为,lockObject.wait()和lockObject.notify(),看wait源码中的注释就知道了,要求wait必须在synchronized代码块中使用,notify也是如此。

在这里插入图片描述

2、await/signal

使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程,在使用时同样需要注意以下两个问题:

同样,直接上代码

public class AwaitAndSignalTest {
    static Lock lock=new ReentrantLock();
    static Condition condition=lock.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
//            try {
//                Thread.sleep(3000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"进来了");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"A").start();


        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"唤醒");
                condition.signal();
            }finally {
                lock.unlock();
            }
        },"B").start();

    }
}

上面代码通过lock和condition实现线程间的通信,同样两个线程,A调用await(),然后B调用signal()唤醒,正常运行下没有问题,打印结果如下

在这里插入图片描述

2.1、要先await后signal

这里面同样把sleep的注释放开,即B线程先signal(),然后A再await(),此时就会出现A一直await而没有人唤醒导致程序无法退出的问题,如图

在这里插入图片描述

2.2、必须在lock中使用

另一个问题,await和signal需要在lock代码块中使用,如果把lock部分注释掉的话,程序运行会报错,如图

在这里插入图片描述

3、LockSupport

LockSupport是一个线程阻塞工具类,用于创建锁和其它同步类的基本线程阻塞原语,所有的方法都是静态方法,其中的park和unpark方法的作用分别是阻塞线程和解除阻塞线程,可以让线程在任意位置阻塞并唤醒,LockSupport底层调用的是Unsafe中的native代码。

使用LockSupport时没有锁块的要求,前面介绍的两种通信方式中关于“ 先唤醒后等待 ”时发生的问题,使用LockSupport后不会再发生,我们直接来看下它的阻塞和唤醒是如何实现的:

public class LockSupportTest {
    public static void main(String[] args) {
        Thread a= new Thread(() -> {
            //try {
            //    Thread.sleep(3000);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
            //}
            System.out.println(Thread.currentThread().getName()+"进来了");
            LockSupport.park();
            //LockSupport.park();

            System.out.println(Thread.currentThread().getName()+"被唤醒");

        },"A");
        a.start();



        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"唤醒");
            LockSupport.unpark(a);
            //LockSupport.unpark(a);
        },"B").start();
    }
}

同样的功能,之前的两种方式都需要有锁块才能执行等待和唤醒的操作,使用lockSupport后,发现不需要使用锁块就能实现线程的等待和通知,运行下看有没有问题,打印结果如下

在这里插入图片描述

3.1、唤醒和等待没有先后顺序的要求

发现确实不需要加锁了,那么我们再来看看先unpark唤醒再park等待,把上面注释的sleep放开后,看看会不会发生同样的问题

在这里插入图片描述

可以看到,之前关于先唤醒后等待发生的问题,使用Locksupport后没有再发生。

3.2、LockSupport实现原理

LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程:

LockSupport类使用了一种名为Permit (许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。

可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
调用一unpark就加1变成1,
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
每个线程都有一个permit, 且最多只有一个,重复调用unpark也不会累积凭证。

下面通过演示来理解unpark不会积累凭证这个问题,如果把代码中注释的 LockSupport.park();和LockSupport.unpark(a);放开后,再执行,此时会发生阻塞的问题,即第一个LockSupport.park();会被唤醒,而第二个LockSupport.park();无法被唤醒,因为下面虽然连续调用了两次unpark,凭证仍然为1,打印结果如图

在这里插入图片描述

除非我们能保证执行一个park,对应执行一个unpark;然后再执行park和unpark,代码示例如下

package lockTest;

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest {
    public static void main(String[] args) {
        Thread a= new Thread(() -> {
           
            System.out.println(Thread.currentThread().getName()+"进来了");
            LockSupport.park();
            try {  //确保执行park后,执行b线程的unpark
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();

            System.out.println(Thread.currentThread().getName()+"被唤醒");

        },"A");
        a.start();



        new Thread(() -> {
            try {			//确保让A线程先执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName()+"唤醒");
            LockSupport.unpark(a);
            try {  //确保第一次unpark后,执行A线程的park
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(a);
        },"B").start();
    }
}

Node类

我们前面提到过,阻塞队列中节点类型为Node,它是AQS中的一个内部类,作用就是以队列的方式来存放线程节点,关于Node类中的成员变量我们需要认识一下,源码如下

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
...

	static final class Node {
		//共享,表示线程以共享的模式等待锁
        static final Node SHARED = new Node();
  
  		//独占,表示线程以独占的模式等待锁
        static final Node EXCLUSIVE = null;

      	//表示线程获取锁的请求被取消了
        static final int CANCELLED =  1;
       
       	//后续线程需要被唤醒
        static final int SIGNAL    = -1;
        
        //表示节点在等待队列中,节点中的线程等待被唤醒
        static final int CONDITION = -2;
       
		//当前线程处于SHARED即共享式下才会使用该字段
        static final int PROPAGATE = -3;
        
        //初始为0,表示队列中线程的状态
        volatile int waitStatus;
        
		//前指针
        volatile Node prev;

        //后指针 
        volatile Node next;

        //表示处于该节点的线程
        volatile Thread thread;
        
		//指向下一个处于CONDITION状态的节点
        Node nextWaiter;
         
		...
	}     

	...

}    

AQS源码阅读

有了上面的前置知识以后,我们再回过头来继续研究AQS,从最常用的ReentrantLock开始下手。

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的,比如ReentrantLock中静态类sync,我们在调用lock.lock()的时候,实际上调用的是sync.lock();调用lock.unlock()的时候,底层调用的是sync.release(1);

所以表面上我们使用的是ReentrantLock,而实际上使用的是Sync这个类,Sync继承自AbstractQueuedSynchronizer,也就是AQS,完成公平锁和非公平锁的操作,如图所示

在这里插入图片描述

单独的一句 Lock lock=new ReentrantLock(); 我们知道,默认是非公平锁,

public class ReentrantLock implements Lock, java.io.Serializable {	
	private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }
	
    //和传false是一样的效果
	public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

顺便看下公平锁和非公平锁的源码实现

//非公平锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


//公平锁
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;
        }

可以明显看出公平锁与非公平锁的唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()方法,它是公平锁加锁时判断等待队列中是否存在有效节点的方法,即判断是否需要排队。

导致公平锁和非公平锁的差异如下:

整个ReentrantLock的加锁过程,可以分为三个阶段:
1、尝试加锁;
2、加锁失败,线程入队列;
3、线程入队列后,进入阻塞状态。

下面我们就通过模拟银行窗口办理业务的情景,来跟踪一下AQS底层的实现细节,代码如下,

/**
 * 通过ReentrantLock 模拟银行窗口办业务,分析AQS源码流程
 */

public class AQSByRTLock {
    static Lock  lock=new ReentrantLock();
    public static void main(String[] args) {

        //A是第一个客户,此时窗口没有人办理业务,A可以直接办理
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("A客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"A").start();

        //B是第二个客户,由于只有一个窗口且此时有人在办理业务,B需要进入等候区进行等待
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("B客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"B").start();


        //C是第三个客户,C也需要进入等候区进行等待,等A办完之后,可以和B争抢办理业务
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("C客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"C").start();
    }
}

假设A线程先来,此时业务窗口没有人,A可以直接去办理,后面的B和C只能等待,那么此时A执行流程如下:

一、lock方法

A执行lock.lock()方法,实际底层执行的是sync.lock()方法;由于默认是非公平锁,所以来到的是NonfairSync.lock()方法,源码如下

	
public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
    
	static final class NonfairSync extends Sync {
        ...
            
		final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        ...
    }
}
	
1.1、compareAndSetState

此时A进来后,它不知道现在是否轮到自己处理业务(也就是不知道state现在的状态是否为0),所以它通过compareAndSetState这个方法去尝试一下,它期待的值是0,也就是窗口当前没有人办理业务;如果是0的话,我就把它改为1,表示窗口有人在办理业务;

当前没有任何人办理业务,所以A如愿以偿,进入窗口办理业务,此时state修改为1

public abstract class AbstractQueuedSynchronizer  extends AbstractOwnableSynchronizer {

    ....
    
	//父类AbstractQueuedSynchronizer中的方法,底层就是调用unsafe类的compareAndSwapInt方法
	protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
    ...
    
}
1.2、setExclusiveOwnerThread

然后执行setExclusiveOwnerThread方法,执行这个方法的目的就是记录一下,占用窗口的线程是谁,其对应源码如下

//AbstractQueuedSynchronizer  继承了	AbstractOwnableSynchronizer

public abstract class AbstractOwnableSynchronizer  implements java.io.Serializable {
    
    ...
    
	protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    
    ...
    
}

所以这个时候A就成功进入到窗口前进行办理业务了。

1.3、acquire

当A在窗口办理业务时,这时B来了,也就是B执行lock.lock()方法,和A一样,它执行的也是sync.lock()方法,最终来到NonfairSync.lock()方法,它进来后也不知道现在自己是否能够办理业务,它也要尝试一下,通过compareAndSetState方法尝试对state进行修改,它期望的值也是0,然后修改为1 ;

这时候,state已经被A修改为1了,所以尝试失败,compareAndSetState返回false,B这个时候就知道,窗口已经有人在办理任务了;它只能等待,执行acquire方法

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    ...
    
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    ...
    
}

在acquire方法中,分别调用了三个方法,我们一个一个来看看这几个方法的作用

1.3.1、tryAcquire

B这个时候还抱有一丝幻想,尝试着抢占,万一A业务办理完了呢,所以调用tryAcquire方法来试试看,但是我们跟进去发现其源码长成这个样子,

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    
    ...
        
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    
    ...
                 
}

一行业务代码没有,直接抛个异常,这让我们看什么呢?

实际上这是一种典型的模板方法设计模式的应用场景,要求所有子类必须实现这个方法,如果没有实现那我就抛异常给你看看。

所以我们继续向下跟,看它的子类,这里我们选择在ReentrantLock中NonfairSync的实现,

在这里插入图片描述

跟进去发现它最终调用的是子类中Sync的nonfairTryAcquire方法,其源码如下所示

	
public class ReentrantLock implements Lock {
    
    ...
    
	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    
    ...
    
}

流程解释如下:

这个时候返回到acquire方法中的if判断 if ( !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),

由于 !tryAcquire(arg) 结果为true,所以需要继续向后执行,在调用acquireQueued方法之前,先调用addWaiter方法,所以我们下面来看addWaiter(Node.EXCLUSIVE)的流程,这里再次明确一下,此时执行该方法的线程是B线程

1.3.2、addWaiter

调用addWaiter(Node.EXCLUSIVE)方法的时候,传入了一个EXCLUSIVE表示独占模式,当执行该方法的时候,就说明,线程需要入队排队了,

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    ...
        
        
   	private Node addWaiter(Node mode) {
        
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
        
    ...
}

执行流程如下:

1.3.2.1 、enq方法
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

双向链表中,也就是等候队列中,第一个节点为虚节点(也叫哨兵节点),它不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

在这里插入图片描述

在这里插入图片描述

后续节点要进入等候队列时,就不用像第一次这样麻烦了,比如C节点,只需要将tail尾指针指向后续进入队列的节点(tail指向C),同时把该节点的前指针指向入队前的尾节点也就是前尾节点(C.prev 指向B),然后让前尾节点的next指向该节点(B.next指向C)即可,如图所示

在这里插入图片描述

1.4、acquireQueued

那么B线程通过addWaiter之后呢就进入到等候队列里面了,接下来就该执行acquireQueued方法了,下面我们来分析下这个方法都干了什么事。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
  
    ...
        
    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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }    
     
    ...
        
}

failed如果为true的话,最后会执行cancelAcquire取消排队;而interrupted如果为true的话,说明发生了故障,这里面我们不分析极端的情况,只看主流程部分,执行流程解析如下:

1.4.1、predecessor