AbstractQueuedSynchronizer 源码分析

奋斗吧
奋斗吧
擅长邻域:未填写

标签: AbstractQueuedSynchronizer 源码分析 Redis博客 51CTO博客

2023-04-02 18:23:34 131浏览

AbstractQueuedSynchronizer 源码分析,1.AQS介绍1.1AQS介绍AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示

1. AQS 介绍

1.1 AQS介绍

AQS 是 AbstractQueuedSynchronizer 的简称。AQS 提供了一种实现阻塞锁和一系列依赖 FIFO 等待队列的同步器的框架,如下图所示。AQS 为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变 state 变量的 protected 方法,这些方法定义了state 是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的 state 变量,但是为了保证同步,必须原子地操作这些变量。

AbstractQueuedSynchronizer 源码分析_赋值


使用一个被 volatile 关键字修饰的 int 类型的 state 变量表示同步状态

AbstractQueuedSynchronizer 源码分析_公平锁_02

通过内置的 FIFO 队列 CLH 完成资源获取的排队工作, 将排队线程对象封装为 Node

AbstractQueuedSynchronizer 源码分析_公平锁_03

1.2 AQS 中的 Node 节点介绍

下面我们来看看 Node 节点各个字段的含义

waitStatus:存储节点的状态,可能的值有

0:同步队列节点初始化时的状态,默认为0

CANCELLED:表示当前节点的线程被中断或者因超时而取消操作,节点为该状态时不可再更新为其他状态了

SIGNAL:表示当前节点需要去唤醒下一个节点

CONDITIOIN:表示该节点属于等待状态(仅在条件队列中使用该状态)

PROPAGATE:表示下一个被获取对象应该无条件传播的状态(仅在共享模式时使用)

prev:存储当前节点上一个节点(前任节点或前驱节点)的指针反向遍历时可以根据尾节点向上遍历

next:存储当前节点下一个节点(后继节点)的指针

thread:存储线程信息

nextWaiter:存储节点的指针,可能出现两种情况

同步队列中,通过将该值设置成SHARED或EXCLUSIVE来区分该节点处于独占模式还是共享模式

条件队列中,用于存储下一个节点的指针(所以条件队列中使用的是单向链表,只能根据节点找到下一节点,无法反向遍历)

Node节点还提供了以下两个方法,以便在AQS中能更方便的使用Node

isShared():判断当前节点是否处于共享模式

predecessor():获取该节点的前任节点

通过 cas 改变 state 值

//返回当前值
    protected final int getState() {
        return state;
    }

	//设置同步值
    protected final void setState(int newState) {
        state = newState;
    }

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

AQS 同时提供了 互斥模式(exclusive)和共享模式(shared) 两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式(如: ReentrantLock、Semaphore ),当然也有同时实现两种模式的同步类,如ReadWriteLock。

2. AQS 和 ReetrantLock 的关系

AQS 是 JUC 实现锁/同步器实现的模板, 这里是一种典型的模板方法模式(行为型设计模式),使用者需要继承这个AQS并重写指定的方法,最后调用AQS提供的模板方法,而这些模板方法会调用使用者重写的方法。

AbstractQueuedSynchronizer 源码分析_初始化_04

3. ReentrantLock 重入锁

重入锁简单来说一个线程可以重复获取锁资源,虽然ReentrantLock不像synchronized关键字一样支持隐式的重入锁,但是在调用lock方法时,它会判断当前尝试获取锁的线程,是否等于已经拥有锁的线程,如果成立则不会被阻塞(下面讲源码的时候会讲到)。

还有ReentrantLock在创建的时候,可以通构造方法指定创建公平锁还是非公平锁。这里是个细节部分,如果知道有公平锁和非公平锁,但是不知道怎么创建,这样还敢说看过源码?

// ReentrantLock 构造方法
// 默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

// 传入true,创建公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

怎么理解公平锁和非公平锁呢?

先对锁进行获取的线程一定先拿到锁,那么这个锁是公平的,反之就是不公平的。

4. ReentrantLock 加锁过程

4.1 加锁和解锁流程

AbstractQueuedSynchronizer 源码分析_初始化_05

下面我们再来一起看看具体的实现

4.2 lock方法

以下就是一个简单锁的演示了,简单的加锁解锁。

public class ReentrantLockTest {

    public static void main(String[] args) {
        // 创建公平锁
        ReentrantLock lock = new ReentrantLock();
        // 加锁
        lock.lock();
        hello();
        // 解锁
        lock.unlock();
    }

    public static void hello() {
        System.out.println("Say Hello World!");
    }
}

既然我们是看加锁的过程,就从lock方法开始,点进去之后看到了调用了sync对象的lock方法,sync是我们ReentrantLock中的一个内部类,并且这个sync继承了AQS这个类。

public void lock() {
    sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    // 抽象方法,由公平锁和非公平锁具体实现
    abstract void lock();

    // ..... 代码省略

}

有两个类对Sync中的lock方法进行了实现,我们先看非公平锁:NonfairSync

AbstractQueuedSynchronizer 源码分析_公平锁_06

lock方法最后调用了acquire方法,并且传入了一个参数,值为:1

/**
 * Performs lock.  Try immediate barge, backing up to normal
 * acquire on failure.
 */
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

AQS 的 acquire 方法代码如下:

public final void acquire(int arg) {
    // 第一个调用了tryAcquire方法,这方法判断能不能拿到锁
    // 强调,这里的tryAcquire的结果,最后是取反,最前面加了 !运算
    if (!tryAcquire(arg) &&
        // 这里有两个方法  addWaiter 和 acquireQueued
        // addWaiter 是入队
        // acquireQueued 是获取锁和进入等待
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

4.3 tryAcquire方法

从acquire()点击tryAcquire方法进去看,AQS为我们提供了默认实现,默认如果没重写该方法,则抛出一个异常,这里就很突出模板方法模式这种设计模式的概念,提供了一个默认实现。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

同样,我们查看非公平锁的实现

AbstractQueuedSynchronizer 源码分析_赋值_07

最后来到了 NonfairSync 对象中的tryAcquire方法

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    // 尝试获取锁 拿到锁了返回:true,没拿到锁返回:false
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

然后会调用 父类 Sync 的 nonfairTryAcquire 方法

/**
 * 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 {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //case 修改为 1 
            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 tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    // ....	
}

tryAcquire方法执行完成,又回到这里:tryAcquire方法拿到锁返回结果:true,没拿到锁返回:false。

一共分两种情况:

  • 第一种情况,拿到锁了,结果为true,通过取反,最后结果为false,由于这里是&&运算,后面的方法则不会进行,直接返回,代码正常执行,线程也不会进入阻塞状态。
  • 第二种情况,没有拿到锁,结果为false,通过取反,最后结果为true,这个时候,if判断会接着往下执行,执行这句代码:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先执行addWaiter方法。
public final void acquire(int arg) {
    // tryAcquire执行完,回到这里
    if (!tryAcquire(arg) &&
        // Node.EXCLUSIVE 这里传进去的参数是为null,在Node类里面
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

4.4 addWaiter方法

我们AQS里面有个head、tail,以及Node子类,AQS在初始化的时候,数据大概是这个样子的,这个时候队列还没初始化的状态,所以head、tail都是为空。

AbstractQueuedSynchronizer 源码分析_后端_08

addWaiiter这个方法总的来说做了什么事呢?

核心作用:把没有获取到的线程包装成Node节点,并且添加到队列中

具体逻辑分两步,判断队列尾部节点是不是为空,为空就去初始化队列,不为空就维护队列关系。

private Node addWaiter(Node mode) {
    // 因为在AQS队列里面,节点元素是Node,所以需要把当前类包装成一个node节点
    Node node = new Node(Thread.currentThread(), mode);
    // 把尾节点,赋值给pred,这里一共分两种两种情况
    Node pred = tail;
    // 判断尾部节点等不等于null,如果队列没有被初始化,tail肯定是个空
    // 反而言之,如果队列被初始化了,head和tail都不会为空
    if (pred != null) {
        // 整个就是维护链表关系
        // 把当前需要加入队列元素的上一个节点,指向队列尾部
        node.prev = pred;
        // CAS操作,如果队列的尾部节点是等于pred的话,就把tail 设置成 node,这个时候node就是最后一个节点了
        if (compareAndSetTail(pred, node)) {
            // 把之前尾部节点的next指向最后的的node节点
            pred.next = node;
            return node;
        }
    }
    // 初始化队列
    enq(node);
    return node;
}

什么情况下不会被初始化呢?

1、线程没有发生竞争的情况下,队列不会被初始化,由tryAcquire方法就可以体现出,如果拿到锁了,就直接返回了。

2、线程交替执行的情况下,队列不会被初始化,交替执行的意思是,线程执行完代码后,释放锁,线程二来了,可以直接获取锁。这种就是交替执行,你用完了,正好就轮到我用了。

4.5 enq方法

这个方式就是为了初始化队列,参数是由addWaiter方法把当前线程包装成的Node节点。

// 整个方法就是初始化队列,并且把node节点追加到队列尾部
private Node enq(final Node node) {
    // 进来就是个死循环,这里看代码得知,一共循环两次
    for (;;) {
        Node t = tail;
        // 第一次进来tail等于null
        // 第二次进来由于下面代码已经把tail赋值成一个为空的node节点,所以t现在不等于null了
        if (t == null) {
            // CAS把head设置成一个空的Node节点
            if (compareAndSetHead(new Node()))
                // 把空的头节点赋值给tail节点
                tail = head;
        } else {
            // 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
            node.prev = t;
            // CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
            if (compareAndSetTail(t, node))  {
                // 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
                t.next = node;
                return t;
            }
        }
    }
}

通过enq代码我们可以得知一个很重要、很重要、很重要的知识点,在队列被初始化的时候,知道队列第一个元素是什么么?如果你认为是要等待线程的node节点,那么你就错了。

通过这两句代码得知,在队列初始化的时候,是new了一个空Node节点,赋值给了head,紧接着,又把head 赋值给tail。

if (compareAndSetHead(new Node()))
    // 把空的头节点赋值给tail节点
    tail = head;

初始化完成后,队列结构应该是这样子的。

AbstractQueuedSynchronizer 源码分析_公平锁_09

队列初始化后,紧接着第二次循环对不对,t就是我们的尾部节点,node就是要被加入队列的node节点,也就是我们所谓要等待的线程的node节点,这里代码执行完后,直接return了,循环终止了。

// 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
node.prev = t;
// CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
if (compareAndSetTail(t, node))  {
    // 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
    t.next = node;
    return t;
}

AbstractQueuedSynchronizer 源码分析_赋值_10

记住,这里队列初始化的时候,第一个元素是空,队列里面存在两个元素。


好了,最终addWaiter方法会返回一个初始化并且已经维护好,队列关系的Node节点出来。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        // addWaiter返回Node,紧接着调用acquireQueued 方法
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

4.5 acquireQueued方法

既然没拿到锁,就让线程进入阻塞状态,但是肯定不是直接就阻塞了,还需要经过一系列的操作,看源码:

// node == 需要进队列的节点、arg = 1
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)) {
                // 如果能进入到这里,则代表前面那个打饭的人已经搞完了,可以轮第一个排队的人打饭了
                // 既然前面那个人打完饭了,就可以出队列了,会把thread、prev、next置空,等待GC回收
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 返回false,整个acquire方法返回false,就出去了
                return interrupted;
            }
            // 如果不是头部节点,就要过来等待排队了
            // shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 队列阻塞,整个线程就等待被唤醒了 
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

看代码得知,如果当前传进来的节点的上一个节点,是等于head,那么又会调用tryAcquire方法,这里体现的就是自旋获取锁,为什么要这么做呢?

是为了避免进入阻塞的状态,假设线程一已经获取到锁了,然后线程二需要进入阻塞,但是由于线程二还在进入阻塞状态的路上,线程一就已经释放锁了。为了避免这种情况,第一个排队的线程,有必要在阻塞之前再次去尝试获取锁。

假设一:假设我们线程二在进入阻塞状态之前,尝试去获取锁,哎,竟然成功了,则会执行一下代码:

// 调用方法,代码在下面
setHead(node);
p.next = null; // help GC

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

如果拿到锁了,队列的内容,当然会发送变化,由图可见,我们会发现一个问题,队列的第一个节点,又是一个空节点。

因为当拿到锁之后,会把当前节点的内容,指针全部赋值为null,这也是个小细节哟。

AbstractQueuedSynchronizer 源码分析_后端_11

假设二:如果当前节点的上一个节点,不是head,那么很遗憾,没有资格去尝试获取锁,那就走下面的代码。

在进入阻塞之前,会调用shouldParkAfterFailedAcquire方法,这个方法小编先告诉你,由于我们这里是死循环对吧,这个方法第一次调用会放回false,返回false则不会执行执行后续代码,再一次进入循环,经过一些列操作,还是没有资格获取锁,或者获取锁失败,则又会来到这里。

当第二次调用shouldParkAfterFailedAcquire方法,会返回ture,这个时候,线程才会调用parkAndCheckInterrupt方法,将线程进入阻塞状态,等待锁释放,然后被唤醒!!

// 如果不是头部节点,就要过来等待排队了
// shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
    // 队列阻塞,整个线程就等待被唤醒了 
    parkAndCheckInterrupt())
    interrupted = true;

4.5 parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {
    // 在这里被park,等待unpark,如果该线程被unpark,则继续从这里执行
    LockSupport.park(this);
    // 这个是获取该线程是否被中断过,这句代码需要结合lockInterruptibly方法来讲,小编就不详细说了,不然一篇文章讲太多了~~~~
    return Thread.interrupted();
}

到这里我们ReentrantLock整个加锁的过程,就相当于讲完啦,但是这才是最最最简单的一部分,因为还有很多场景没考虑到。

4.6 shouldParkAfterFailedAcquire方法

上面说为什么这个方法第一次调用返回false,第二次调用返回ture ?

这个方法主要做了一件事:把当前节点的,上一个节点的waitStatus状态,改为 - 1。

当线程进入阻塞之后,自己不会把自己的状态改为等待状态,而是由后一个节点进行修改。

举个例子:你躺在床上睡觉,然后睡着了,这个时候,你能告诉别人你睡着了吗?当然不行,因为你已经睡着了,呼噜声和打雷一样,怎么告诉别人。只有当后一个人来了,看到你在呼呼大睡,它才可以告诉别人你在睡觉。

//pred 当前上一个节点,node 当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 第一次循环进来:获取上一个节点的线程状态,默认为0
    // 第二次循环进来,这个状态就变成-1了,
    int ws = pred.waitStatus;
    // 判断是否等于-1,第一进来是0,并且会吧waitStatus状态改成-1,代码在else
    // 第二次进来就是-1了,直接返回true,是当前线程进行阻塞
    if (ws == Node.SIGNAL)
        /*
        * This node has already set status asking a release
        * to signal it, so it can safely park.
        */
        return true;
    // 判断是否大于0,waitStatus分几种状态,这里其他几种状态的源码就不一一讲了。
    // = 1:由于在同步队列中等待的线程,等待超时或者被中断,需要从同步队列中取消等待,该节点进入该状态不会再变化
    // = -1:后续节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,使后续节点继续运行
    // = -2:节点在等待队列中,节点线程在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态中获取
    // = -3:表示下一次共享式同步状态获取将会无条件地被传播下去
    // = 0 :初始状态
    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;
        // 因为默认0,所以第一次会走到else方法里面    
    } else {
        // CAS吧waitStatus修改成-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false,外层方法接着循环操作
    return false;
}

5. ReentrantLock 解锁过程

5.1 unlock方法

讲完加锁过程,就来解锁过程吧,说实话,看源码这种经历,必须要自己花时间去看,去做笔记,去理解,大脑最好有个整体的思路,这样才会印象深刻。

public class ReentrantLockTest {

    public static void main(String[] args) {
        // 创建公平锁
        ReentrantLock lock = new ReentrantLock(true);
        // 加锁
        lock.lock();
        hello();
        // 解锁
        lock.unlock();
    }

    public static void hello() {
        System.out.println("Say Hello");
    }
}

点击unlock解锁的方法,会调用到release方法,这个是AQS提供的模板方法,再来看tryRelease方法。

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

最终调用的 release 方法如下

public final boolean release(int arg) {
    // tryRelease 释放锁,如果真正释放会把当前持有锁的线程赋值为空,否则只是计数器-1
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

5.1 tryRelease方法

发现又是个抽象类,我们选择ReentrantLock类实现的

AbstractQueuedSynchronizer 源码分析_赋值_12

这里要注意:

1、当前解锁的线程,必须是持有锁的线程

2、state状态,必须是等于0,才算是真正的解锁,否则只是代表重入次数-1.

protected final boolean tryRelease(int releases) {
    // 获取锁计数器 - 1
    int c = getState() - releases;
    // 判断当前线程 是否等于 持有锁的线程,如果不是则抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 返回标志
    boolean free = false;
    // 如果计算器等于0,则代表需要真正释放锁,否则是代表重入次数-1
    if (c == 0) {
        free = true;
        // 将持有锁的线程赋值空
        setExclusiveOwnerThread(null);
    }
    // 重新设置state状态
    setState(c);
    return free;
}

执行完tryRelease方法,返回到release,进行if判断,如果返回false,就直接返回了,否则进行解锁操作。

public final boolean release(int arg) {
    // tryRelease方法返回true,则表示真的需要释放锁
    if (tryRelease(arg)) {
        // 如果是需要真正释放锁,先获取head节点
        Node h = head;
        // 第一种情况,假设队列没有被初始化,这个时候head是为空的,则不需要进行锁唤醒
        // 第二种情况,队列被初始化了head不为空,并且只要有线程在队列中排队,waitStatus在被加入队列之前,会把当前节点的上一个节点的waitStatus改为-1
        // 所以只有满足h != null && h.waitStatus != 0 这个条件表达式,才能真正代表有线程正在排队
        if (h != null && h.waitStatus != 0)
            // 解锁操作,传入头节点信息
            unparkSuccessor(h);
        return true;
    }
    return false;
}

5.2 unparkSuccessor方法

这里的参数传进来的是head的node节点信息,真正解锁的线程是head.next节点,然后调用unpark进行解锁。

private void unparkSuccessor(Node node) {
    // 先获取head节点的状态,应该是等于-1,原因在shouldParkAfterFailedAcquire方法中有体现
    int ws = node.waitStatus;
    // 由于-1会小于0,所以重新改为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 获取第一个正常排队的队列
    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);
    }
}

如果这里调用unpark,线程被唤醒,会接着这个方法接着执行。到这里整个解锁的过程小编就讲完了。

private final boolean parkAndCheckInterrupt() {
    // 在这里被park,等待unpark,如果该线程被unpark,则继续从这里执行
    LockSupport.park(this);
    // 这个是获取该线程是否被中断过,这句代码需要结合lockInterruptibly
    return Thread.interrupted();
}

6. ReentrantLock 公平锁

6.1 hasQueuedPredecessors方法

看到这里,小编希望是小伙伴真的理解了ReentrantLock加锁和解锁的过程,并且在心里有整体流程,不然你看这个方法,会很蒙,这个方法虽然代码几行,但是要完全理解,比较困难。

// 不要小看以下几行代码,涉及的场景比较复杂
public final boolean hasQueuedPredecessors() {
    // 分别把尾节点、头节点赋值给 t、h
    Node t = tail;
    Node h = head;
    Node s;
    // AQS队列如果没有发生竞争,刚开始都是未初始化的,所以一开始tail、head都是为null
    // 第一种情况:AQS队列没有初始化的情况
    // 假设线程一,第一个进来,这个时候t、h都是为null,所以在h != t,这个判断返回false,由于用的是&&所以整个判断返回fase
    // 返回flase表示不需要排队。
    // 但是也不排除可能会有两个线程同时进来判断的情况,假设两个线程发现自己都不需要排队,就跑去CAS进行修改计数器,这个时候肯定会有一个失败的
    // CAS 是可以保证原子性操作的,假设线程一它CAS成功了,那么线程二就会去初始化队列,老老实实排队去了

    // 第二种情况:AQS队列被初始化了
    // 场景一:队列元素大于1个点情况,假设有一个线程在排队,在队列中应该有而个元素,一个是头节点、线程2
    // 现在线程2之前的线程已经执行完,并且释放锁唤醒线程2.线程2又会继续醒来循环。并且线程二是第一个排队的,所以有资格获取锁
    // 只要是获取锁就会来排队需不需要排队,代码又回到这里
    // 现在 h = 等于头节点,而 tail = 线程2的node节点,所以 h != t 结果为true
    // h表示头节点,并且h.next是线程2的节点,所以 (s = h.next) == null 返回 flase
    // s 等于 h.next,也就是线程2的节点信息,并且当前执行的线程也是线程2,所以 s.thread != Thread.currentThread(),返回false
    // 最后return的结果是 true && false,结果为false,代表不需要排队

    // 场景二:队列元素等于1个,什么情况下队列被初始化了并且只有一个元素呢?
    // 当有线程竞争初始化队列,之后队列又全部都被消费完了。最后剩下一个为空的node,并且head和tail都指向它
    // 这个时候有新的线程进来,其实h != t,直接返回false,因为head 和tail都指向最后一个节点了

    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

流程图如下:

AbstractQueuedSynchronizer 源码分析_后端_13

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695