JUC - 底层原理 - AQS

浅谈Java并发编程系列(九)—— AQS结构及原理分析

我没看太精通,只是自己当笔记,大家慎看

1. AQS是干什么的

1.1 在历史长河中

最开始在jdk1.6以前只有synchronized的重量级锁,调用的是底层的native方法,也就是会去调用操作系统的函数,因为要操作操作系统的函数所以有用户态–>内核态的状态的切换,所以是重量级

这个时候Doung Lea大哥看不惯了,开发了JUC这个包,让解决同步不要再去操作系统层面去做,可想而知比synchronized更快;对于JUC并发包下的同步类(LockSemaphoreReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架

AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用

后来又升级到jdk1.7,sun公司也去升级了synchronized,也就是升级了轻量级锁,偏向锁

1.2 从代码层面看看AQS是干嘛的

就像上面说的AQS是一个抽象类,主是是以继承的方式使用,在之前学习的锁,信号量等你会发现他们很相似都是为了让部分线程等待让线程协作,同步,既然有这样的共同点,就能为他们都提取出一个工具类,可以复用,对于ReentrantLockSemaphore而言就可以屏蔽很多细节,只关注自己的业务逻辑就行了

对于Semaphore就是这样的,在内部有一个Sync类,Sync类继承了AQS,实现了部分方法
在这里插入图片描述
在这里插入图片描述
我们去看源码你会发现ReentrantLock 等都是使用了AQS的技术底层使用自旋+CAS+park这三大技术栈实现的,而且在单个线程或线程交替执行的时候,是在jdk级别解决同步问题,且和队列无关

在这里插入图片描述

1.3 AQS的作用

  • 同步状态的原子性管理
  • 线程的阻塞与解除阻塞
  • 队列的管理
    在并发的场景下,我们正确并高效的实现这些内容是相当复杂的,所以我们使用AQS来帮助我们搞定,自己只需要关心业务逻辑

AQS是一个用于构建锁,同步器,协作工具类的工具类(框架),有了AQS后,更多的协作工具都可以很方便的被写出来

有了AQS,构建线程协作类就容易多了
在这里插入图片描述

2. AQS的基本使用

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

2.1 AQS的核心部分

① state状态
② 控制线程抢锁和配合的FIFO队列
③ 期望协作工具类去实现的获取/释放等重要方法

2.1.1 state状态
/**
     * The synchronization state.
     */
    private volatile int state;

state的具体含义会根据具体实现类的不同而不同

  • Semaphore中,他表示"剩余的许可证是数量"
  • CountDownLatch中,他表示"还需要倒数的数量"
  • ReentrantLock中,state用来表示锁的占有情况,包括可重入计数

state方法都是volatile修饰的, 会被并发地修改,所以所有的修改state的方法都需要保证线程安全,比如getStatesetState,以及compareAndSetState操作来读取和更新这个状态,这些方法都依赖于j.u.c.atomic包的支持

2.1.2 控制线程抢锁和配合的FIFO队列

这个队列是用来存放等待线程的,AQS就是排队管理器,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起,当锁释放的时候,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁

AQS会维护一个等待的线程队列(双向链表形式),把线程都放到在这个队列里

     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
2.1.3 期望协作工具类去实现的获取/释放等重要方法

获取
获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候)
Semaphore中,获取就是acquire方法,作用是获取一个许可证
CountDownLatch中,获取是await方法,作用是等待,直到倒数结束

释放方法
释放操作不会阻塞
在Semaphore中,释放就是release方法,作用是释放一个许可证
在CountDownLatch中,释放就是countDown方法,作用是倒数一个数

2.2 AQS的用法

AQS详解
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样
① 第一步:写一个类,想好协作逻辑,实现获取/释放方法
② 第二步:内部写一个Sync类继承AbstractQueuedSynchronizer,根据是否独占来重写tryAcquire()/tryRelease()或者tryAcquireShared(int acquires)/tryReleaseShared(int release)等方法,在之前写的获取/释放方法中调用AQS的acquire()/release()或者`shared``方法
③ 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法

自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回falsetryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回falsetryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

获取锁的姿势

//如果获取锁失败
if(!tryAcquire(arg)){
	//入队,可以选择阻塞当前线程 park
}

释放锁的姿势

//如果释放锁成功
if(tryRelease(arg)){
	//让阻塞的线程恢复运行
}

使用AQS实现互斥锁

class Mutex implements Lock, java.io.Serializable {
    // 自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判断是否锁定状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 尝试获取资源,立即返回。成功则返回true,否则false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 这里限定只能为1个量
            if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
                return true;
            }
            return false;
        }

        // 尝试释放资源,立即返回。成功则为true,否则false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定为1个量
            if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//释放资源,放弃占有状态
            return true;
        }
    }

    // 真正同步类的实现都依赖继承于AQS的自定义同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //unlock<-->release。两者语文一样:释放资源。
    public void unlock() {
        sync.release(1);
    }

    //锁是否占有状态
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

2.3 CountDowmLatch中对AQS的使用

内部Sync继承AQS
在这里插入图片描述
构造方法

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

await方法
进行等待直到倒数结束

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)//判断当前剩余倒数数量是否大于0
        doAcquireSharedInterruptibly(arg);//获取锁不成功 把当前线程放入阻塞队列并阻塞 AQS中方法
}

//tryAcquireShared被CountDownLatch中的Sync重写
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,stateCAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作

2.4 Semaphor中对AQS的使用

  • Semaphore中,state表示许可证的剩余数量
  • tryAcquire方法,判断nonfairTryAcquireShared大于等于0的话,代表成功
  • 这里会先检查剩余的许可证数量够不够这次需要的,用减法来计算,如果直接不够,那就返回负数,表示失败,如果够了,就用自旋+CAS来改变state的状态,直到改变成功就返回正数;或者是期间如果被其他人修改了导致剩余数量不够 , 那也返回负数代表获取失败

2.5 ReentrantLock中对AQS的使用

state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的

3. 如果你是Doung Lea

上面说了如何使用AQS框架,但是AQS内部的线程阻塞和唤醒究竟应该怎么去实现呢?

3.1 自旋锁

为了去在java层面解决并发的锁的问题,我们可以使用自旋,声明一个锁对象,锁对象中有一个int类型,初始值为0的state成员变量标识这个锁是否已经被占用了;想要进行线程同步的线程都来拿这把锁,拿到了就把state变量置为1,而没有拿到的就去自旋等待

对于上面想法的问题在于,对state的赋值操作也会发生竞争,所以我们需要一个原子的方法完成对state的赋值,可以想到使用cas操作

public class MyReeLock {
    volatile int state= 0;//注意是volatile保证可见性
    void lock(){
        while(!compareAndSwap(0,1)){
            //自旋
        }
    }
    
    void unlock(){
        state = 0;
    }

    boolean compareAndSwap(int expected, int newValue){
        //cas操作,修改成功返回true
        //使用unsafe对象
    }
}

使用

MyReeLock myReeLock = new MyReeLock();
	myReeLock.lock();
	try {
		//do something
	}finally {
        myReeLock.unlock();
	}

3.2 yield+自旋

上面的实现的缺点是耗费cpu资源,得不到锁的线程一直在空转,我们可以让得不到锁的线程让出cpu

public class MyReeLock {
    volatile int state= 0;//注意是volatile保证可见性
    void lock(){
        while(!compareAndSwap(0,1)){
            yield();//让出cpu
        }
    }
    
    void unlock(){
        state = 0;
    }

    boolean compareAndSwap(int expected, int newValue){
        //cas操作,修改成功返回true
        //使用unsafe对象
    }
}

3.3 park+自旋

上面的问题在于yield只是让线程进入就绪状态下一次可能依旧是它获得CPU,为了让出CPU我们可以使用park方法,既能让出CPU又能被叫醒

在这个方案我们还加入一个队列parkQueue用来存放没有竞争到锁的线程,方便唤醒

public class MyReeLock {
    volatile int state= 0;
    Queue<Thread> parkQueue;
    void lock(){
        while(!compareAndSwap(0,1)){
            park();
        }
        //lock
    }

    void unlock(){
        state = 0;
        lock_notify();
    }

    void lock_notify(){
        LockSupport.unpark(parkQueue.poll());
    }

    void park(){
        parkQueue.add(Thread.currentThread());
        LockSupport.park();
    }

    boolean compareAndSwap(int expected, int newValue){
        //cas操作,修改成功返回true
    }

    public static void main(String[] args) {
        MyReeLock myReeLock = new MyReeLock();
        myReeLock.lock();
        try {
            //do something
        }finally {
            myReeLock.unlock();
        }
    }
}

4. AQS源码

Java并发之AQS详解

4.1 ReentrantLock基本实现

上面说到AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用

接下来以ReentrantLock的源码入手来深入理解下AQS的实现。在ReentrantLock类中,有一个Sync成员变量,是继承了AQS的子类

 public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }
}

这里的Sync也是一个抽象类,其实现类为FairSyncNonfairSync,分别对应公平锁和非公平锁。
在这里插入图片描述
ReentrantLock的提供一个入参为boolean值的构造方法,来确定使用公平锁还是非公平锁

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

再看下面源码前我们先回想一下我们自己实现的锁的要点

① 竞争锁通过对一个条件变量的state的cas操作实现

AbstractQueuedSynchronizer维护了一个volatile int类型的变量,用户表示当前同步状态。volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性,compareAndSetState的实现依赖于UnsafecompareAndSwapInt()方法

② 没有竞争到锁的线程要被park

j.u.c.locks包提供了LockSupport类来解决这个问题。方法LockSupport.park阻塞当前线程直到有个LockSupport.unpark方法被调用。unpark的调用是没有被计数的,因此在一个park调用前多次调用unpark方法只会解除一个park操作。另外,它们作用于每个线程而不是每个同步器。一个线程在一个新的同步器上调用park操作可能会立即返回,因为在此之前可以有多余的unpark操作。但是,在缺少一个unpark操作时,下一次调用park就会阻塞。虽然可以显式地取消多余的unpark调用,但并不值得这样做。在需要的时候多次调用park会更高效。park方法同样支持可选的相对或绝对的超时设置,以及与JVMThread.interrupt结合 ,可通过中断来unpark一个线程

③ 被阻塞的线程要放到阻塞队列等待被唤醒
队列就是网上一直在说的CLH(Craig,Landin,and Hagersten)队列,这是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系(双向链表)。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配

④ 要实现可重入

4.2 看看公平锁

这里以NonfairSync类为例,看下它的Lock()的实现:

     final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
     }
加锁成功

lock方法先通过CAS尝试将同步状态(AQSstate属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程。看下compareAndSetState()setExclusiveOwnerThread()实现:

     protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
     }

可以看到compareAndSetState底层其实是调用的unsafeCAS系列方法。

     protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

exclusiveOwnerThread属性是AQS从父类AbstractOwnableSynchronizer中继承的属性,用来保存当前占用同步状态的线程

所以你能看到在单个线程使用AQS技术加锁,或者是多个线程交替使用只涉及到了一个CAS操作,和队列根本无关,而且是在java层面不涉及OS,这也就是为什么jdk1.6的时候CAS一出现就打败了重量级锁Synchronized

加锁失败

如果CAS操作未能成功,说明state已经不为0,此时继续acquire(1)操作,这个acquire()由AQS实现提供:

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

代码很短,不太好了理解,转换下写法(代码1):

   public final void acquire(int arg) {
        boolean hasAcquired = tryAcquire(arg);
        if (!hasAcquired) {
            Node currentThreadNode = addWaiter(Node.EXCLUSIVE);
            boolean interrupted = acquireQueued(currentThreadNode, arg);
            if (interrupted) {
                selfInterrupt();
            }
        }
    }

tryAcquire方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。

首先看tryAcquire(arg)NonfairSync中的实现(这里arg=1):

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        
        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;
        }

首先获取AQS的同步状态(state),在锁中就是锁的状态,如果状态为0,则尝试设置状态为arg(这里为1), 若设置成功则表示当前线程获取锁,返回true。这个操作外部方法lock()就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁

如果状态不为0,再判断当前线程是否是锁的owner(即当前线程在之前已经获取锁,这里又来获取),如果是owner, 则尝试将状态值增加acquires,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。这里可以看非公平锁的涵义,即获取锁并不会严格根据争用锁的先后顺序决定。这里的实现逻辑类似synchroized关键字的偏向锁的做法**,即可重入而不用进一步进行锁的竞争,也解释了ReentrantLockReentrant的意义**

如果状态不为0,且当前线程不是owner,则返回false

回到上面的代码1,tryAcquire返回false,接着执行addWaiter(Node.EXCLUSIVE),这个方法创建结点并入队

看一下AQS队列结构

AQS(AbstractQueuedSynchronizer)类的设计主要代码(具体参考源码)

private transient volatile Node head; //队首 其实队首Node的Thread属性永远为空,看下面的代码实现
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0

在这里插入图片描述
如果是你来实现一个队列来对线程进行排队和管理,你需要关心什么信息呢?

线程,肯定要知道我是哪个线程(因为连哪个线程都不知道,你还排啥队,管理个球球?)
队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)

Node类的设计

public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;//该变量类型为Thread对象,表示该节点的代表的线程
    int waitStatus;//该int变量表示线程在队列中的状态
    //CANCELLED:值为1,表示线程的获锁请求已经“取消”
	//SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
	//CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
	//PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上
}
再来看一下入队操作
private Node addWaiter(Node mode) {
    //由于AQS队列当中的元素类型为Node,故而需要把当前线程tc封装成为一个Node对象,下文我们叫做nc
    Node node = new Node(Thread.currentThread(), mode);
    //tail为队尾,赋值给pred 
    Node pred = tail;
    //判断pred是否为空,其实就是判断队尾是否有节点,其实只要队列被初始化了队尾肯定不为空,假设队列里面只有一个元素,那么队尾和队首都是这个元素
    //换言之就是判断队列有没有初始化
    //上面我们说过代码执行到这里有两种情况,1、队列没有初始化和2、队列已经初始化了
    //pred不等于空表示第二种情况,队列被初始化了,如果是第二种情况那比较简单
   //直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
   //继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了
    if (pred != null) {
        //直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
        node.prev = pred;
        //这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
        if (compareAndSetTail(pred, node)) {
            //继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了 对应第11行注释
            pred.next = node;
            //然后把nc返回出去
            return node;
        }
    }
    //如果上面的if不成立就会执行到这里,表示第一种情况队列并没有初始化
    enq(node);
    //返回nc
    return node;
}

enq(node)方法,从字面可以看出这是一个入队操作,来看下具体入队细节

  private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //队尾复制给t,上面已经说过队列没有初始化,故而第一次循环t==null(因为是死循环,因此强调第一次,后面可能还有第二次、第三次,每次t的情况肯定不同)
        Node t = tail;
        //第一次循环成立
        if (t == null) { // Must initialize
            //new Node就是实例化一个Node对象下文我们成为nn,调用无参构造方法实例化出来的Node里面三个属性都为null
            //入队操作--compareAndSetHead继而把这个nn设置成为队列当中的头部,cas防止多线程、确保原子操作;记住这个时候队列当中只有一个,即nn
            if (compareAndSetHead(new Node()))
                //这个时候AQS队列当中只有一个元素,即头部=nn,所以为了确保队列的完整,设置头部等于尾部,即nn即是头也是尾,而且是一个成员变量是为null的node,这点记不记得之前说的对队头的thread一直为null
                //然后第一次循环结束
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
//为了方便 第二次循环我再贴一次代码来对第二遍循环解释
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //对尾复制给t,由于第二次循环,故而tail==nn,即new出来的那个node
        Node t = tail;
        //第二次循环不成立
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //不成立故而进入else
            //首先把nc,当前线程所代表的的node的上一个节点改变为nn,因为这个时候nc需要入队,入队的时候需要把关系维护好
            //所谓的维护关系就是形成链表,nc的上一个节点只能为nn,这个很好理解
            node.prev = t;
            //入队操作--把nc设置为队尾,队首是nn,
            if (compareAndSetTail(t, node)) {
                //上面我们说了为了维护关系把nc的上一个节点设置为nn
                //这里同样为了维护关系,把nn的下一个节点设置为nc
                t.next = node;
                //然后返回t,即nn,死循环结束,这个返回其实就是为了终止循环,返回出去的t,没有意义
                return t;
            }
        }
    }
}

  //这个方法已经解释完成了
  enq(node);
  //返回nc,不管哪种情况都会返回nc;到此addWaiter方法解释完成
  return node;
 }

方法体是一个死循环,本身没有锁,可以多个线程并发访问,假如某个线程进入方法,此时head, tail都为null, 进入if(t==null)区域,从方法名可以看出这里是用CAS的方式创建一个空的nn作为头结点,因为此时队列中只一个头结点,所以tail也指向它,第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时,只有一个线程能够执行成功,防止创建多个同步队列。

进行第二次循环时(或者是其他线程enq时),tail不为null,进入else区域。将当前线程的Node结点(简称nc)的prev指向tail,然后使用CAS将tail指向nc。看下这里的实现


    private final boolean compareAndSetTail(Node expect, Node update) {
            return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
        }

expect为t, t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向CNode,返回头结点。经过上面的操作,头结点和CNode的关系如图:
在这里插入图片描述
其他线程再插入节点以此类推,都是在追加到链表尾部,并且通过CAS操作保证线程安全。

通过上面分析可知,AQS的写入是一种双向链表的插入操作,至此addWaiter分析完毕。

以上入队就完成了,那么接下来要做的是什么? – park

park

addWaiter返回了插入的节点,作为acquireQueued方法的入参
入完队要看看这个过程中是不是有线程释放锁了,所以入队了的线程在park前要看看是不是能获得锁,如果能获得就不用park了直接拿锁就好了,这也就是自旋

但是这个自旋不是所以的都自旋,因为这是公平锁,所以只有最前面的才能自旋,才有机会获得锁

final boolean acquireQueued(final Node node, int arg) {//这里的node 就是当前线程封装的那个node 下文叫做nc
    //记住标志很重要
    boolean failed = true;
    try {
        //同样是一个标志 打断
        boolean interrupted = false;
        //死循环
        for (;;) {
            //获取nc的上一个节点,有两种情况;1、上一个节点为头部;2上一个节点不为头部
            final Node p = node.predecessor();
            //如果nc的上一个节点为头部,则表示nc为队列当中的第二个元素,为队列当中的第一个排队人;这里的第一和第二不冲突;我上文有解释;
            //如果nc为队列当中的第二个元素,第一个排队的则调用tryAcquire去尝试假设---关于tryAcquire看上面的分析
            //只有nc为第二个元素;第一个排队的情况下才会尝试加锁,其他情况直接去park了,因为第一个排队的执行到这里的时候需要看看持有有锁的线程有没有释放锁,释放了就轮到我了,就不park了
            //有人会疑惑说开始调用tryAcquire加锁失败了(需要排队),这里为什么还要进行tryAcquire不是重复了吗?
            //其实不然,因为第一次tryAcquire判断是否需要排队,如果需要排队,那么我就入队;当我入队之后我发觉前面那个人就是第一个,那么我不死心,再次问问前面那个人搞完没有
            //如果搞完了,我就不park,接着他搞;如果他没有搞完,那么我则在队列当中去park,等待别人叫我
            //但是如果我去排队,发觉前面那个人在睡觉,前面那个人都在睡觉,那么我也睡觉把
            
            if (p == head && tryAcquire(arg)) {
                //能够执行到这里表示我来加锁的时候,锁被持有了,我去排队,进到队列当中的时候发觉我前面那个人没有park,前面那个人就是当前持有锁的那个人,那么我问问他搞完没有
                //能够进到这个里面就表示前面那个人搞完了;所以这里能执行到的几率比较小;但是在高并发的世界中这种情况真的需要考虑
                //如果我前面那个人搞完了,我nc得到锁了,那么前面那个人直接出队列,我自己则是对首;这行代码就是设置自己为对首
                setHead(node);
                //这里的P代表的就是刚刚搞完事的那个人,由于他的事情搞完了,要出队;怎么出队?把链表关系删除
                p.next = null; // help GC
                //设置表示---记住记加锁成功的时候为false
                failed = false;
                //返回false;为什么返回false 为了不调用50行---acquire方法当中的selfInterrupt方法;为什么不调用?下次解释比较复杂
                return interrupted;
            }
            //进到这里分为两种情况
            //1、nc的上一个节点不是头部,说白了,就是我去排队了,但是我上一个人不是队列第一个
            //2、第二种情况,我去排队了,发觉上一个节点是第一个,但是他还在搞事没有释放锁
            //不管哪种情况这个时候我都需要park,park之前我需要把上一个节点的状态改成park状态
            //这里比较难以理解为什么我需要去改变上一个节点的park状态呢?每个node都有一个状态,默认为0,表示无状态
            //-1表示在park;但是为什么不能自己把自己改成-1状态?为什么呢?因为你得确定你自己park了才是能改为-1;不然你自己改成自己为-1;但是改完之后你没有park那不就骗人?
            //所以只能先park;在改状态;但是问题你自己都park了;完全释放CPU资源了,故而没有办法执行任何代码了,所以只能别人来改;故而可以看到每次都是自己的后一个节点把自己改成-1状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                //改上一个节点的状态成功之后;自己park;到此加锁过程说完了
                parkAndCheckInterrupt())//也就是park
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire(Node pred, Node node):在我加锁失败一次后要不要再自旋还是直接休眠

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//前一个的状态,默认值为0,-1表示休眠
        if (ws == Node.SIGNAL)//Node.SIGNAL=-1
        	//前一个在休眠,那我肯定也休眠,不然我能多自旋一次
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//把前一个的waitStatus改成-1
        }

在这里插入图片描述
shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,则应当通知它,所以它要阻塞了,返回true

如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉

如果shouldParkAfterFailedAcquire返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作

再看解锁

先提一句,“不管公平还是非公平模式下,ReentrantLock对于排队中的线程都能保证,排在前面的一定比排在后面的线程优先获得锁”但是,这里有个但是,非公平模式不保证“队列中的第一个线程一定就比新来的(未加入到队列)的线程优先获锁” 因为队列中的第一个线程尝试获得锁时,可能刚好来了一个线程也要获取锁,而这个刚来的线程都还未加入到等待队列,此时两个线程同时随机竞争,很有可能,队列中的第一个线程竞争失败(而该线程等待的时间其实比这个刚来的线程等待时间要久)

既然申请锁的时候会导致线程在得不到锁时被“阻塞”,那么,肯定就是其他线程在释放锁时“唤醒”被阻塞着的线程去“拿锁”

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

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

其中,unparkSuccessor(h)方法就是“唤醒操作”,主要流程如代码所示

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //如果head节点的下一个节点它是null或者已经被cancelled了(status>0)
    //那么就从队列的尾巴往前找,找到一个最前面的并且状态不是cancelled的线程
    //至于为什么要从后往前找,不是从前往后找,谁能跟我说一下,这点我也不知道为什么
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //将找到的队列中符合条件的第一个线程“唤醒”
    if (s != null)
        LockSupport.unpark(s.thread);
}

① 尝试释放当前线程持有的锁
② 如果成功释放,那么去唤醒头结点的后继节点(因为头节点head是不保存线程信息的节点,仅仅是因为数据结构设计上的需要,在数据结构上,这种做法往往叫做“空头节点链表”。对应的就有“非空头结点链表”)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值