队列同步器AQS

队列同步器概念

队列同步器(AbstractQueuedSynchronizer),简称同步器或AQS。它为实现锁提供了一个框架,内部维护了一个先进先出的队列,以及state状态变量ReentrantLockReentrantReadWriteLockSemaphore(信号量)CountDownLatch公平锁非公平锁ThreadPoolExecutor等都和AQS有关!

AQS是用来构建锁或其他同步组件的基础框架

  • 使用一个volatile的int类型的成员变量,来表示同步状态private volatile int state;

    • state=0时,则说明没有任何线程占有共享资源的锁;当state=1时,则说明线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。

    • 通过CAS完成对State值的修改,来实现多线程的独占模式(0-1)和共享模式(0-N)。

  • AQS通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作

  • 同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列;而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁竞争

注意,这里涉及到两种队列:

  • 一种的同步队列,当线程请求锁而等待后,将加入同步队列等待;

  • 另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。

在锁的实现类中会聚合同步器,然后利用同步器实现锁的语义:

  • 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节!

  • 同步器面向的是锁的实现者——Daug Lea、自定义同步器,它简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待/唤醒等底层操作!

从AQS的类名称和修饰上来看,这是一个抽象类,所以从设计模式的角度来看,同步器一定是基于模版模式来设计的,使用者需要继承同步器实现自定义同步器,并重写指定方法,随后将同步器组合在自定义的同步组件中,并调用同步器的模版方法,而这些模版方法又回调使用者重写的方法。

想理解上面这句话,只需要知道下面两个问题:

  • 哪些是自定义同步器可重写的方法

  • 哪些是抽象同步器提供的模版方法

同步器可重写的方法

自定义同步器实现的相关方法,只是为了通过修改state字段,来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):

方法名 描述
protected boolean isHeldExclusively() 该线程是否正在独占资源。只有用到Condition才需要去实现它。
protected boolean tryAcquire(int arg) 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True。
protected boolean tryRelease(int arg) 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True。
protected int tryAcquireShared(int arg) 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg) 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时,实现独占和共享两种方式,如ReentrantReadWriteLockReentrantLock是独占锁,所以实现了tryAcquire-tryRelease

一般需要重写的方法也应该有abstract来修饰,自定义的同步组件或者锁不可能既是独占式又是共享式,为了避免强制重写不相干方法,所以就没有abstract来修饰了,但要抛出异常告知不能直接使用该方法:

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

同步状态State方法

同步状态就是用volatile修饰的state——用于展示当前临界资源的获锁情况,所以在重写上面表格中的几个方法时,还要通过同步器提供的下面三个方法(AQS提供的)来获取或修改同步状态

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    /**
     * The synchronization state.
     */
    private volatile int state;
    
    protected final int getState() {
        return state;
    }
    
    protected final void setState(int newState) {
        state = newState;
    }
    
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

ReentrantLock内部维护了一个volatile修饰的变量state,通过CAS来进行读写(最底层还是交给硬件来保证原子性和可见性),如果CAS更改成功,即获取到锁,线程进入到try代码块继续执行;如果没有更改成功,线程会被【挂起】,不会向下执行。

  • compareAndSetState通常用于在获取到锁之前,尝试加锁时,对state进行修改。这种场景下,由于当前线程不是锁持有者,所以对state的修改是线程不安全的,也就是说可能存在多个线程都尝试修改state。所以需要保证对state修改的原子性操作,即使用了unsafe类的本地CAS方法
  • setState方法通常用于当前正持有锁的线程对state变量进行修改,不存在竞争,是线程安全的,所以此处没必要用CAS保证原子性,修改的性能更重要。

这几个方法都是final修饰的,说明子类中无法重写它们

可以通过修改state字段表示的同步状态,来实现多线程的独占模式共享模式(加锁过程):

对于自定义的同步工具,需要自定义获取同步状态释放状态的方式,也就是AQS架构图中的第一层:API层。

ReentrantLockReentrantReadWriteLockSemaphore(信号量)CountDownLatch这几个类其实仅仅是在实现以上几个方法上略有差别,其他的实现都是通过同步器的模版方法来实现的

同步器提供的模版方法

上面将同步器的实现方法分为独占式和共享式两类,模版方法其实除了提供以上两类模版方法之外,只是多了响应中断超时限制的模版方法供Lock使用:

注意:上面的方法都有final关键字修饰,说明子类不能重写这个方法

自定义互斥锁

package mylock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyMutex implements Lock {
    /**
     * 静态内部类-自定义同步器
     */
    private static class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            // 调用AQS提供的方法,通过CAS保证原子性
            // 通过CAS设置state状态,不管成功与否都会马上返回
            if (compareAndSetState(0, arg)) {
                /*
                 * 实现的是互斥锁,所以标记获取到同步状态(更新state成功)的线程,主要为了判断是否可重入
                 */
                setExclusiveOwnerThread(Thread.currentThread());
                // 获取同步状态成功,返回true
                return true;
            }
            // 获取同步状态失败,返回false
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            // 未拥有锁,却让释放,会抛出IllegalMonitorStateException
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 可以释放,清空排它线程标记
            setExclusiveOwnerThread(null);
            // 设置同步状态为0,表示释放锁
            setState(0);
            return true;
        }

        /**
         * 是否独占式持有
         * @return true of false
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        /**
         * 主要用于等待/通知机制,每个condition都有一个与之对应的条件等待队列
         * @return condition
         */
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    /**
     * 聚合自定义同步器
     */
    private final MySync sync = new MySync();

    @Override
    public void lock() {
        // 阻塞式的获取锁,调用同步器模版方法独占式,获取同步状态
        // 调用AbstractQueuedSynchronizer#tryAcquire(自定义)
        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();
    }
}

ReentrantLockReentrantReadWriteLockSemaphore(信号量)CountDownLatch都是按照这个结构实现的。

AQS架构图

  • 上图中有颜色的为Method,无颜色的为Attribution。

  • AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API,到底层基础数据。

  • 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层

下面会从整体到细节,从流程到方法逐一剖析AQS框架,主要分析过程如下:

AQS同步队列

AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock)。无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列

当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点,并将线程当前必要的信息存储到node结点中,然后加入同步队列等待获取锁

这系列操作都有AQS协助完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的。

因此,AQS核心思想可表述为

  • 如果被请求的共享资源空闲,那么就将当前请求资源的线程,设置为有效的工作线程,将共享资源设置为锁定状态

  • 如果共享资源被占用,就需要一定的阻塞等待唤醒机制,来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列

即,AQS使用一个volatile的int类型的成员变量,来表示同步状态private volatile int state;),通过内置的FIFO队列,来完成资源获取的排队工作,通过CAS完成对State值的修改

CLH(Craig、Landin and Hagersten)是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程,封装成一个节点,来实现锁的分配

主要原理图如下:

队列中每个排队的个体就是一个Node!

Node节点及waitStatus

Node结点是对每一个访问同步代码的线程的封装,它包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /** waitStatus value to indicate the next acquireShared should unconditionally propagate */
        static final int PROPAGATE = -3;

        volatile int waitStatus;
        
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
}

其中SHAREDEXCLUSIVE常量分别代表共享模式独占模式

  • 所谓共享模式一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的;

  • 独占模式则是同一个时间段,只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock

变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE:

  • CANCELLED:值为1,同步队列中等待的线程,等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。即,处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行

  • CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁

  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态

  • 0状态:代表初始化状态

prenext,分别指向当前Node结点的前驱结点和后继结点;thread变量存储请求锁的线程。nextWaiter与Condition相关,代表等待队列中的后继结点

同步队列的结构

AQS内部维护了一个同步队列,用于管理同步状态。

  • 当线程获取同步状态失败时,就会将当前线程,以及等待状态等信息,构造成一个Node节点,将其加入到同步队列中尾部阻塞该线程

  • 当同步状态被释放时,会唤醒同步队列中首节点的线程获取同步状态。

headtail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构,这样可方便队列进行结点增删操作。

state变量则是代表同步状态

  • 当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功

  • 如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待

非公平锁加锁与解锁过程

加锁过程

1.ReentrantLock#lock

final void lock() {
    // 执行CAS操作,获取同步状态
    if (compareAndSetState(0, 1))
        // 成功则将独占锁线程设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 否则再次请求同步状态
        acquire(1);
}
  • 首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时(CAS操作保证设置state变量的原子性),CAS操作只能保证一个线程操作成功,剩下的只能去排队

  • 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入acquire方法进行后续处理

2.AbstractQueuedSynchronizer#acquire

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 再次尝试获取同步状态
    public final void acquire(int arg) {
        // 调用自定义同步器重写的tryAcquire方法
        // 如果获取锁失败,就会调用addWaiter加入到等待队列中去
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    protected boolean tryAcquire(int arg) { // 点击tryAcquire,选择ReentrantLock#tryAcquire
        throw new UnsupportedOperationException();
    }
}

acquire方法干了三件事情:

  • tryAcquire体现了不公平):会尝试再次通过CAS获取一次锁;非公平锁ReentrantLock#tryAcquire的流程是:

    • 检查state字段,若为0,表示此时锁未被占用(先前被占用的已经被释放了),那么尝试占用;
    • 若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数
    • 如果以上两点都没有成功,则获取锁失败,返回false。
  • addWaiter:将当前线程加入双向链表(同步队列)中;

  • acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。

3.AbstractQueuedSynchronizer#addWaiter
当执行acquire(1)时,会通过tryAcquire获取锁。即,会再次尝试非阻塞的获取同步状态,如果获取失败(tryAcquire返回false),则会调用addWaiter方法构造Node节点(由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE)用于封装线程及其相关信息,并加入同步队列尾部

同步队列中tail是AQS的成员变量,指向队尾。如果即将加入的节点是第一个节点,则为tail肯定为空,那么将执行enq(node)操作,进行初始化。如果非第一个节点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node)enq(node)通过“死循环”确保节点被正确添加,最终将其设置为尾节点之后,才会返回。

4.acquireQueued挂起

addWaiter方法把对应的线程以Node的数据结构形式,加入到双端队列里返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行获锁操作

即,添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机,待条件满足,获取同步状态,然后从同步队列退出,并结束自旋,回到之前的acquire()方法。自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的。

for(;;) {
  // ...
  if (p == head && tryAcquire(arg))
}

总的来说,一个线程获取锁失败了,被放入同步队列,acquireQueued会把放入队列中的线程不断去获取锁直到获取成功或者不再需要获取(中断)被挂起!

for(;;) {
  //...
  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 
      interrupted = true;
}
  • shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态)

    • 如果是则返回true。

    • 如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用,应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。

    • 倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。

  • parkAndCheckInterrupt:如果前驱节点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true ,同时又不是head节点,程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起,称为WAITING状态需要等待一个unpark()操作来唤醒它

解锁过程

代码实现独占式获取同步状态

lock(ReentrantLock)

// 自己实现
public class MyMutex implements Lock {
    public void lock() {
        // 阻塞式的获取锁,调用同步器模版方法独占式,获取同步状态
        sync.acquire(1);
    }
}

// ReentrantLock具体实现
public class ReentrantLock implements Lock, java.io.Serializable {
    // 默认创建非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // #1
    public void lock() {
        sync.lock();  // 点击进入abstract void lock();
    }
    // #2
    abstract void lock();
    
    // #3 内部类非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * 非公平锁加锁流程
         */
        final void lock() {
            // 执行CAS操作,获取同步状态
            if (compareAndSetState(0, 1))
                // 成功则将独占锁线程设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 否则再次请求同步状态
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
}

这块代码的含义为:

  • 通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程

    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    
    • 首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时(CAS操作保证设置state变量的原子性),CAS操作只能保证一个线程操作成功,剩下的只能去排队

    • 非公平即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就插队

  • 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入acquire方法进行后续处理

第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?

有以下两种可能:

  • 将当前线程获锁结果设置为失败,获取锁流程结束。这种设计会极大降低系统的并发度,并不满足实际的需求。所以就需要下面这种流程,也就是AQS框架的处理流程。

  • AQS框架的处理流程存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续

    • 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
    • 处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
    • 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题?

acquire(AbstractQueuedSynchronizer)

acquire方法干了三件事情:

  • tryAcquire体现了不公平):会尝试再次通过CAS获取一次锁;非公平锁ReentrantLock#tryAcquire的流程是:

    • 检查state字段,若为0,表示此时锁未被占用(先前被占用的已经被释放了),那么尝试占用;
    • 若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数
    • 如果以上两点都没有成功,则获取锁失败,返回false。
  • addWaiter:将当前线程加入上面锁的双向链表(同步队列)中;

  • acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 再次尝试获取同步状态
    public final void acquire(int arg) {
        // 调用自定义同步器重写的tryAcquire方法
        // 如果获取锁失败,就会调用addWaiter加入到同步队列中去
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    protected boolean tryAcquire(int arg) { // 点击tryAcquire,选择ReentrantLock#tryAcquire
        throw new UnsupportedOperationException();
    }
}

这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值),因为要获取锁,而status为0时是释放锁,1则是获取锁,所以这里一般传递参数为1。进入方法后首先会执行tryAcquire(arg)方法,该方法在AQS中并没有具体实现,而是交由子类实现

tryAcquire(ReentrantLock):如何体现非公平

public class ReentrantLock implements Lock, java.io.Serializable {
    
    static final class NonfairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    /**
     * 非公平即体现在这里,如果占用锁的线程【刚释放锁】,state置为0,
     * 而【排队等待锁的线程还未唤醒】时,【新来的线程】就直接抢占了该锁,那么就【插队】了
     */
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取state变量值
        int c = getState();
        
        // 1.没有线程占用锁
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {  // 执行CAS操作
                setExclusiveOwnerThread(current); // 占用锁成功,设置独占线程为当前线程
                return true;
            }
        }
        // 2.如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
        else if (current == getExclusiveOwnerThread()) { 
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 设置当前同步状态,当前只有一个线程持有锁,
            // 因为不会发生线程安全问题,可以直接执行setState(nextc)
            setState(nextc); 
            return true;
        }
        // 3.获取锁失败
        return false;
    }
}

nonfairTryAcquire做了两件事:

  • 一是尝试再次获取同步状态,如果没有线程占用锁,并获取成功,则将当前线程设置为OwnerThread;

  • 二是判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。

需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功,即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态,然后新到来的线程恰好获取到同步状态)先获取到锁。

公平锁与非公平锁tryAcquire方法

非公平锁与公平锁最大的区别:

  • 公平锁在线程请求到来时先会判断同步队列是否存在节点如果存在先执行同步队列中的节点线程,当前线程将封装成node加入同步队列等待

  • 非公平锁:当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源。在绝大多数情况下,非公平锁才是理想的选择,毕竟从效率上来说,非公平锁总是胜于公平锁。

hasQueuedPredecessors是公平锁加锁时判断同步队列中是否存在有效节点的方法

  • 如果返回False,说明当前线程可以争取共享资源;

  • 如果返回True,说明队列中存在有效节点,当前线程必须加入到同步队列中

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
}

h != t时:

  • 如果(s = h.next) == null同步队列正在有线程进行初始化,但只是进行到了Tail指向Head没有将Head指向Tail此时队列中有元素,需要返回True。
  • 如果(s = h.next) != null,说明此时队列中至少有一个有效节点
    • 如果此时s.thread == Thread.currentThread(),说明同步队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的
    • 如果s.thread != Thread.currentThread(),说明同步队列的第一个有效节点线程与当前线程不同当前线程必须加入进同步队列

该方法与nonfairTryAcquire(int acquires)方法唯一的不同是,在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在,必须先执行完同步队列中结点的线程**,当前线程进入等待状态

addWaiter(enq)线程加入队列

当执行acquire(1)时,会通过tryAcquire获取锁。即,会再次尝试非阻塞的获取同步状态,如果获取失败(tryAcquire返回false),则会调用addWaiter方法构造Node节点(由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE)用于封装线程及其相关信息,并加入同步队列尾部

同步队列中tail是AQS的成员变量,指向队尾。如果即将加入的节点是第一个节点,则为tail肯定为空,那么将执行enq(node)操作。如果非第一个节点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    /**
     * Creates and enqueues node for current thread and given mode.
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
	// 将请求同步状态失败的【线程】,以及【节点模式(独占模式)】封装成【结点】
        Node node = new Node(Thread.currentThread(), mode); 
        // 新建变量pred,将指针指向tail指向的节点
        Node pred = tail;
        /**
         * 如果是【第一个结点】加入肯定为空,跳过。
         * 如果【非第一个结点】则直接【执行compareAndSetTail入队操作】,尝试在【尾部】快速添加
         */
        if (pred != null) {
            node.prev = pred; // 【新加入的节点】的前驱节点,指向原来的尾节点
            /*
             * 因为如果【多个线程】同时获取同步状态【失败】,都会执行这段代码。
             * 所以,通过CAS方式,确保安全的设置当前节点为最新的尾节点
             */
            if (compareAndSetTail(pred, node)) {
                pred.next = node; // 【曾经的尾节点】的后继节点指向当前节点
                return node; // 返回新构建的节点
            }
        }
        
        /*
         * 如果Pred指针是Null(说明等待队列中没有元素),【或者】当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改)
         * 
         * 如果【第一次加入】或者【compareAndSetTail操作没有成功】执行enq入队操作
         */
        enq(node);
        return node;
    }
    
    
    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        // 对tailOffset和Expect进行比较,
        // 如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    
    /**
     * 【第一次加入】初始化队列并且入队新节点,或者将【compareAndSetTail操作没有成功】的节点执行enq入队操作
     */
    private Node enq(final Node node) {
        // 通过“死循环”确保节点被正确添加,最终将其设置为尾节点之后才会返回
        for (;;) {
            Node t = tail;
            // 如果尾节点为null,队列为空
            if (t == null) { // Must initialize
                // 构建一个哨兵节点,并将头部指针指向它
                // 如果tail为空,则新建一个head节点,并且tail指向head
                if (compareAndSetHead(new Node())) // 创建并使用CAS设置头结点
                    tail = head;
            } else {
                // 将新节点的前驱节点指向t
                node.prev = t;
                if (compareAndSetTail(t, node)) { // 将新节点加入到队列尾节点
                    t.next = node; // 前驱节点的后继节点指向当前新节点,完成双向队列
                    return t;
                }
            }
        }
    }
}

addWaiter#enq

该方法做了两件事:

  • 一是,如果还没有初始同步队列,则创建新结点并使用compareAndSetHead设置头结点,tail也指向head;

  • 二是队列已存在,则将新结点node添加到队尾

这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作,进行头结点设置尾结点tail替换可以保证线程安全

从这里也可以看出head结点本身不存在任何数据,它只是作为一个哨兵节点,而tail永远指向尾部结点(前提是队列不为null)。

这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功

假设线程A入队成功,之后B、C开始新一轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败,还需要一轮循环。最终所有线程都可以成功入队。当B、C入等待队列后(A不在队列里),此时AQS队列如下:

acquireQueued

addWaiter方法把对应的线程以Node的数据结构形式,加入到双端队列里返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行获锁操作

即,添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机,待条件满足,获取同步状态,然后从同步队列退出,并结束自旋,回到之前的acquire()方法。自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的。

总的来说,一个线程获取锁失败了,被放入同步队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)被挂起!

1.acquireQueued方法的流程

一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)

跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程):

2.acquireQueued源码

下面我们从“何时出队列?”和“如何出队列?”两个方向来分析一下acquireQueued源码:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    final boolean acquireQueued(final Node node, int arg) {
        // 标记是否成功拿到资源(锁)
        boolean failed = true;
        try {
            // 标记等待过程中是否中断过
            boolean interrupted = false;
            // "死循环",尝试获取锁,或者挂起
            for (;;) {
                // 获取【当前节点】的【前驱节点】
                final Node p = node.predecessor();
                // 只有【当前节点】的【前驱节点】是【头节点】,才会尝试获取锁(哨兵节点作用)
                // 如果p是头结点,说明【当前节点】在真实数据队列的【首部】,就尝试获取锁(头结点是虚节点)
                // 跳出当前循环的条件是,当“前置节点是头结点,且当前线程获取锁成功”
                if (p == head && tryAcquire(arg)) {
                    // 注意这一步操作!
                    // 【获取同步状态成功,将获得锁的当前节点设置为head节点——虚节点】(头指针移动到当前node)
                    setHead(node); 
                    p.next = null; // 原head节点出队(原哨兵节点的后继节点置为空),在某个时间点被GC回收
                    failed = false; // 获取成功
                    return interrupted;  // 返回是否被中断过
                }
                /*
                 * 判断获取失败后是否可以挂起,若可以,则挂起
                 *
                 * 说明“p不为头结点”【或者】“当前节点没有获取到锁”(可能是由于非公平锁,被抢占了)
                 *【或者】是“p不是头节点”且“当前节点没有获取到锁”
                 * 这个时候就要判断当前node是否要被阻塞【被阻塞条件:前驱节点的waitStatus为-1:SIGNAL】,
                 * 防止无限循环浪费资源
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&  
                    parkAndCheckInterrupt()) 
                    interrupted = true; // 线程若被中断,设置interrupted为true
            }
        } finally {
            if (failed)
                cancelAcquire(node); // 最终都没能获取同步状态,结束该线程的请求
        }
    }
}

当前线程在自旋(死循环)中获取同步状态当且仅当,当前结点的前驱结点头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出。

其次node是当前获取同步状态的线程结点,只有当node释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态。后继结点,在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起

注意:

  • 当线程请求锁而等待后,将加入同步队列等待(等待被唤醒);

  • 通过Condition调用await()方法释放锁后,将加入等待队列

3.setHead

进入第一个if语句后调用setHead(node)方法,将当前线程结点设置为head。setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。

node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后,唤醒后继结点

4.shouldParkAfterFailedAcquire

如果前驱结点不是head,或者,假设B和C在竞争锁的过程中,A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt()将获取同步状态失败的线程挂起

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 判断当前结点的前驱结点是否为SIGNAL状态(即【等待唤醒状态】)
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱节点(头节点)的状态
        int ws = pred.waitStatus;
        // 如果是SIGNAL状态:-1,即等待被占用的资源释放(说明头结点处于唤醒状态),直接返回true
  	// 准备继续调用parkAndCheckInterrupt方法
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release to signal it, so it can safely park.
             */
            return true;
        
        // ws > 0说明前驱节点是CANCELLED状态:1
        if (ws > 0) {
            // 循环判断前驱节点的前驱节点是否也为CANCELLED状态,忽略该状态的节点,重新连接队列
            do {
                // 循环向前查找取消节点,把所有取消节点从队列中剔除
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将【当前节点】的【前驱节点】设置为SIGNAL状态,用于后续唤醒操作
      	    // 程序第一次执行到这返回为false,还会进行外层第二次循环
            /*
             * 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);
        }
        return false;
    }
}

shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态)

  • 如果是则返回true。

  • 如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用,应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。

  • 倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。

5.parkAndCheckInterrupt

if (shouldParkAfterFailedAcquire(p, node) &&  
                    parkAndCheckInterrupt()) 
        interrupted = true; // 线程若被中断,设置interrupted为true

只有shouldParkAfterFailedAcquire(p, node)true,程序才会去继续执行parkAndCheckInterrupt方法。

所以,线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。

如果前驱节点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true ,同时又不是head节点,程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起,称为WAITING状态需要等待一个unpark()操作来唤醒它

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态
    private final boolean parkAndCheckInterrupt() {
        // 将当前线程挂起
        LockSupport.park(this);
        /**
         * 根据park方法API描述,程序在下述三种情况会继续向下执行
         * 1.被unpark;2.被中断(interrupt);3.其他不合逻辑的返回才会继续向下执行
         * 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态
         * 如果由于被中断(不再处于WAITING状态),该方法会返回true
         */
        return Thread.interrupted();
    }
}

被唤醒的程序会继续执行acquireQueued方法里的循环(for(;;)"死循环",尝试获取锁,或者挂起),如果获取同步状态成功,则会返回interrupted = true的结果。

public final void acquire(int arg) {
    // 调用自定义同步器重写的tryAcquire方法
    // 如果获取锁失败,就会调用addWaiter加入到同步队列中去
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

代码实现独占式释放同步状态

解锁的流程**:先尝试释放锁,

  • 若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程

  • 如果释放失败那么返回false,表示解锁失败。

每次都只唤起头结点的下一个节点关联的线程。

unlock

ReentrantLock在解锁的时候,并不区分公平锁和非公平锁!

public class ReentrantLock implements Lock, java.io.Serializable {
    public void unlock() {
        sync.release(1);
    }
}

AQS#release

调用AQS模版方法release

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    public final boolean release(int arg) {
        // 如果返回true,说明该锁没有被任何线程持有
        if (tryRelease(arg)) {
            // 释放成功,获取头节点
            Node h = head;
            // 存在头节点,并且waitStatus不是初始状态
      	    // 在获取的过程中会将waitStatus的值从初始状态更新成SIGNAL状态
            if (h != null && h.waitStatus != 0)
                // 解除线程挂起状态
                unparkSuccessor(h);  // 唤醒后继结点的线程
            return true;
        }
        return false;
    }
    
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
}

if (h != null && h.waitStatus != 0):

  • h == null Head还没初始化:初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null的情况。
  • h != null && waitStatus == 0:表明后继节点对应的线程仍在运行中,不需要唤醒
  • h != null && waitStatus < 0:表明后继节点可能被阻塞了,需要唤醒

1.ReentrantLock#Sync#tryRelease

public class ReentrantLock implements Lock, java.io.Serializable {
    /**
     * 在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 释放当前线程占用的锁,返回是否释放成功
        protected final boolean tryRelease(int releases) {
            // 减少可重入次数(计算释放后state值)
            int c = getState() - releases;
            // 当前线程不是持有锁的线程,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state
            if (c == 0) { // 锁被重入次数为0,表示释放成功
                free = true;
                setExclusiveOwnerThread(null); // 清空独占线程
            }
            setState(c); // 更新state值
            return free;
        }
    }
}

tryRelease是ReentrantLock类中内部类自己实现的,因为AQS对于释放锁并没有提供具体实现,必须由子类自己实现,其过程为:

  • 当前释放锁的线程若不持有锁,则抛出异常。

  • 若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且清空独占线程,最后更新state值,返回free。

2.release#unparkSuccessor

unparkSuccessor方法作用是,要唤醒头节点的后继节点

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    private void unparkSuccessor(Node node) {
        // 获取头节点的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0); // 清空头节点的waitStatus值,即置为0
        Node s = node.next; // 获取当前节点的后继节点——下一个需要唤醒的结点
        // 判断当前节点的后继节点是否是【取消状态】或者【null】,如果是,需要移除,重新连接队列
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从尾节点向前查找,找到队列第一个waitStatus状态小于0的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 如果是独占式,这里小于0,其实就是SIGNAL
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
        if (s != null)
            // 解除线程挂起状态
            LockSupport.unpark(s.thread);
    }
}

为什么这个地方是从队列尾部向前查找第一个非CANCELLED的节点?

  • 原因一:由节点加入队列的情景可知:
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;  // 1
        if (compareAndSetTail(pred, node)) { // 2
            pred.next = node; // 3
            return node;
        }
    }
    enq(node);
    return node;
}

节点入队并不是原子操作

node.prev = pred; 
compareAndSetTail(pred, node)

这两个地方可以看作是尾节点入队的原子操作,如果此时代码还没执行到pred.next = node;(第1、2和3之间并非原子操作),这时又恰巧执行了unparkSuccessor方法,就没办法从前往后找了,因为后继指针还没有连接起来,所以需要从后往前找。

  • 原因二:在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

综上所述,如果是从前往后找,由于极端情况下入队的非原子操作CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点

独占式tryLock

非阻塞式的获取锁,尝试获取,获取不到不会阻塞,直接返回false:

public class ReentrantLock implements Lock, java.io.Serializable {
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    /**
     * 非公平即体现在这里,如果占用锁的线程【刚释放锁】,state置为0,
     * 而【排队等待锁的线程还未唤醒】时,【新来的线程】就直接抢占了该锁,那么就【插队】了
     */
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取state变量值
        int c = getState();
        
        // 1.没有线程占用锁
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {  // 执行CAS操作
                setExclusiveOwnerThread(current); // 占用锁成功,设置独占线程为当前线程
                return true;
            }
        }
        // 2.如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
        else if (current == getExclusiveOwnerThread()) { 
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 设置当前同步状态,当前只有一个线程持有锁,
            // 因为不会发生线程安全问题,可以直接执行setState(nextc)
            setState(nextc); 
            return true;
        }
        // 3.获取锁失败
        return false;
    }
}

nonfairTryAcquire工作流程:

  • 检查state字段,若为0,表示此时锁未被占用(先前被占用的已经被释放了),那么尝试占用;
  • 若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数
  • 如果以上两点都没有成功,则获取锁失败,返回false。

需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功,即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态,然后新到来的线程恰好获取到同步状态)先获取到锁。

独占式tryLock(timeout, unit)

tryLock(long timeout, TimeUnit unit)提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。

那么超时的功能是怎么实现的呢?

public class ReentrantLock implements Lock, java.io.Serializable {
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        // 调用同步器模版方法,可响应中断和超时时间限制
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

tryAcquireNanos

  • 如果线程被中断了,那么直接抛出InterruptedException。

  • 如果未中断:

    • 先尝试获取锁,获取成功就直接返回

    • 获取失败则进入doAcquireNanos

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
}

doAcquireNanos

  • 线程先进入同步队列

  • 然后开始自旋,尝试获取锁

    • 获取成功就返回;

    • 失败则在队列里找一个安全点把自己挂起,直到超时时间过期。

这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    /**
     * 在有限的时间内去竞争锁
     * @return 是否获取成功
     */
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        // 计算超时截止时间
        final long deadline = System.nanoTime() + nanosTimeout;
        // 以独占方式加入到同步队列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            // 自旋
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 计算新的超时时间
                nanosTimeout = deadline - System.nanoTime();
                // 1.如果超时,直接返回false
                if (nanosTimeout <= 0L)
                    return false;
                // 2.需要挂起,且超时时间未到
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 判断是最新超时时间是否大于阈值1000
                    // static final long spinForTimeoutThreshold = 1000L;
                    nanosTimeout > spinForTimeoutThreshold)
                    // 挂起线程,时间到(nanosTimeout),自动返回
                    // 阻塞当前线程直到超时时间到期
                    LockSupport.parkNanos(this, nanosTimeout);
                // 3.响应中断
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

独占式响应中断获取同步状态lockInterruptibly

public interface Lock {
    void lockInterruptibly() throws InterruptedException;
}

public class ReentrantLock implements Lock, java.io.Serializable {
    public void lockInterruptibly() throws InterruptedException {
        // 调用同步器模版方法可中断式获取同步状态
        sync.acquireInterruptibly(1);
    }
}

AQS#acquireInterruptibly

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试非阻塞式获取同步状态,如果没有获取到同步状态,执行doAcquireInterruptibly
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
}

doAcquireInterruptibly

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer implements java.io.Serializable {
    
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 获取中断信号后,不再返回interrupted = true的值,
                    // 而是直接抛出InterruptedException 
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

参考资料

1.Java AQS队列同步器以及ReentrantLock的应用
2.从ReentrantLock的实现看AQS的原理及应用

posted @ 2021-06-27 11:02  chenzufeng  阅读(114)  评论(0)    收藏  举报