锁:AbstractQueuedSynchronizer源码解析(上)

本文深入剖析了AbstractQueuedSynchronizer(AQS)的源码,详细解读了AQS的整体架构,包括同步队列和条件队列的工作原理,以及共享锁和排它锁的区别。同时,对AQS的同步器状态进行了讲解,并重点分析了获取排它锁和共享锁的流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


AbstractQueuedSynchronizer是同步器,简称AQS,是各类线程锁的基础。该类的方法很多,针对包装线程的节点的操作也很多。

1.整体架构

AQS整体处理流程如下,
image
对上图进行说明,

  1. AQS中的队列有两种,同步队列和条件队列,其底层数据结构是链表
  2. 四种颜色的线条代表不同的场景

AQS本身就是一套锁的框架,定义了获得锁和释放锁的代码,所以继承AQS抽象类并实现相应的方法即可实现锁。

类注释

AQS的类注释中包含的信息如下,

  1. AQS提供了一个框架,定义了先进先出的同步队列,让获取不到锁的线程在同步队列中排序
  2. 同步器存在一个成员变量status,表示同步器的状态,用于判断AQS是否能够得到锁。该变量使用volatile关键字进行修饰保证其线程安全
  3. AQS的子类可以通过CAS的方式给status赋值,定义哪些状态可以获取锁,哪些状态获取不到锁
  4. AQS提供两种锁模式,共享锁和排它锁。
    i) 排他模式:只有一个线程可以获得锁
    ii)共享模式:多个线程可以同时获得锁
    AQS的子类 ReadWriteLock实现了这两种模式
  5. AQS的内部类ConditionObject是条件队列的实现类。通过new Condition()可以创建一个条件队列。锁对象中,条件队列的数目可以是多个
  6. AQS继承了 AbstractOwnableSynchronizer类,该类可以追踪获得锁的线程

类定义

AQS的类定义如下,

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {}
  1. AQS是抽象类,该类中只实现类将线程放入和取出同步队列、条件队列的方法,定义了如何获得锁、如何释放锁的抽象方法,目的是让子类去实现争锁和释放锁的过程
  2. 继承了AbstractOwnableSynchronizer类,该类的作用就是为了知道哪个线程获取了锁,便于管理

类属性

1)一般属性

// 同步器状态,子类会根据状态字段进行判断是否可以获得锁或释放锁
// CAS给该变量赋值,获得锁+1,释放锁-1
private volatile int state;

// 自旋超时阀值,单位纳秒。当设置等待时间时才会用到这个属性
static final long spinForTimeoutThreshold = 1000L;

2)同步队列属性

同步队列的说明如下,

  1. 当多个线程都来请求锁时,某一时刻有且只有一个线程能够获得锁(排它锁)。其余获取不到锁的线程,都会到同步队列中去排队并阻塞自己。
  2. 当有线程主动释放锁时,就会从同步队列头开始释放一个排队的线程,让线程重新去竞争锁。
  3. 同步队列的底层是双向链表
private transient volatile Node head;

private transient volatile Node tail;

同步队列的头和尾,是AQS的属性。

3) 条件队列属性

条件队列的说明如下,

  1. 条件队列同样用于管理获取不到锁的线程
  2. 条件队列的底层是单向链表(与同步队列区分)
  3. 条件队列不直接和锁打交道
public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    // 条件队列中第一个 node
    private transient Node firstWaiter;
    // 条件队列中最后一个 node
    private transient Node lastWaiter;
}  

条件队列实现的是 Condition接口。

4)Node

Node既是同步队列的节点,同时也是条件队列的节点。该类用于包装线程,

static final class Node {
    // 同步队列单独的属性
    //node共享模式或排他模式
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 当前节点的前节点
    // 节点 acquire 成功后就会变成head
    // head 节点不能被 cancelled
    volatile Node prev;

    // 当前节点的下一个节点
    volatile Node next;

    //  两个队列共享的属性
    // 表示当前节点的状态,通过节点的状态来控制节点的行为
    // 普通同步节点,就是 0 ,条件节点是 CONDITION -2
    volatile int waitStatus;
    // waitStatus 的状态有以下几种
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    // 当前节点包装的线程
    volatile Thread thread;

    // 在同步队列中,nextWaiter并不真的是指向其下一个节点,只是表示当前 Node 是排它模式还是共享模式
    // 但在条件队列中,nextWaiter 就是表示下一个节点元素
    Node nextWaiter;
}

Node类中的waitStatus一定与AQS的status进行区分,Node的操作是通过waitStatus决定的。

对Node节点的状态进行如下说明,

状态说明
CANCELLED被取消
SIGNAL同步队列中的节点在自旋获取锁的时候, 如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了;否则自己一直自旋尝试获得锁
CONDITION表示当前 node 正在条件队列中。当节点从同步队列转移到条件队列时,状态就会被更改成 CONDITION
PROPAGATE无条件传播,共享锁模式下,该状态的进程处于可运行状态

5)共享锁和排它锁的区别

同一时刻只能有一个线程可以获得排它锁,也只能有一个线程可以释放锁。

共享锁同一时刻允许多个线程获得同一个锁,并可以设置获取锁的线程的数目。

Condition接口

条件队列ConditionObject实现了Condition接口,

1)类注释

  1. 当使用锁对象替代 synchronize时,Condition用于替代 Object中的监控方法,如Object#wait()Object#notify()Object#notifyAll()等方法
  2. 线程被暂停执行后,等待其他线程将其唤醒
  3. Condition实例绑定在锁对象上,通过Lock#newCondition()方法可以创建锁的条件队列

2)条件队列示例

假设存在一个有边界的队列,支持 put和 take方法存入或取出元素,

  1. 如果试图向空队列执行take操作,线程将会阻塞,直到队列中有可用的元素为止
  2. 如果试图向满队列执行put操作,线程将会阻塞,直到队列中有空闲的位置为止

上面的例子中,如果两个操作依靠一个条件队列,那么每次只能执行其中一个操作。所以可以创建两个条件队列,分别执行存入和取出的操作。

3)接口方法

Condition接口定义了一些方法,这些方法奠定了条件队列的基础,

void await() throws InterruptedException;

该方法使当前线程一直等待,直到被signalsignalAll方法唤醒。

条件队列中的线程被唤醒的四种情况,

  1. 有线程使用了signal方法,唤醒了条件队列中的当前线程。该方法唤醒条件队列中的一个线程,在被唤醒前必须先获得锁
  2. 有线程使用了signalAll方法,该方法唤醒条件队列中的所有线程
  3. 其他线程打断了当前线程
  4. 虚假唤醒

线程从条件队列中苏醒时,必须重新获得锁,才能真正被唤醒

2.同步器状态

AQS中存在两个状态,statuswaitStatus,二者一定要区分,

  • status是锁的状态,是 int 类型。子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁,比如当前同步器状态是 0,表示可以获得锁,当前同步器状态是 1,表示锁已经被其他线程持有,当前线程无法获得锁;
  • waitStatus 是节点(Node)的状态,种类很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3),各个状态的含义可以见上文。

3.获取锁

在AQS的子类中通常使用Lock#lock()方法获得锁,使得线程能够取得资源的使用权限。Lock是AQS的子类,lock方法或根据情况调用AQS的acquiretryAcquire方法。

acquire 方法 AQS 已经实现了,tryAcquire 方法是等待子类去实现。

  • acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁
  • tryAcquire 方法在 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁

获得排它锁—acquire

public final void acquire(int arg) {
    // tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
    if (!tryAcquire(arg) &&
        // addWaiter 入参代表是排他模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

该流程对应架构图的红线部分,
在这里插入图片描述
具体流程为,

  1. 尝试执行一次tryAcquire方法,如果成功则线程执行任务即可。如果失败则走下面的流程进入等待队列
  2. 调用addWaiter方法将线程包装成Node,添加到同步队列尾部。addWaiter方法返回包装了当前线程的Node对象
  3. 调用acquireQueued方法,该方法的作用有两个,
    i. 阻塞当前线程
    ii. 节点被唤醒时,使其能够获得锁
    这两个功能的实现是依靠acquireQueued方法中自旋过程
  4. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 如果返回值为true说明线程是在同步队列中阻塞一段时间后才被取出,此时线程的interrupt状态为true,调用selfInterrupt()interrupt改为false

1)addWaiter方法

用于将未获取到锁的线程添加到同步队列尾部,

private Node addWaiter(Node mode) {
    // 初始化包装当前线程的Node对象
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果同步队列不为空,先简单尝试将节点加到队列尾部,通常情况下会成功加入
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果队列为空或简单尝试失败则自旋保证node加入到队尾
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
        // tail = head = new Node();
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // 队尾不为空,将当前节点追加到队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在 addWaiter 方法中,并没有进入方法后立马就自旋,而是先尝试一次追加到队尾,如果失败才自旋,因为大部分操作可能一次就会成功,这种思路在写自旋的时候可以借鉴。

2)acquireQueued方法

将线程加入到同步队列尾部后,需要使当前线程阻塞,acquireQueued方法用于实现这一功能,

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            final Node p = node.predecessor();	// 获取当前线程节点的前一个节点
            // 有两种情况会走到 p == head,见下方说明
            if (p == head && tryAcquire(arg)) {
                // 成功获得锁,则将自身设置成head节点并回收其前置节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            // shouldParkAfterFailedAcquire和parkAndCheckInterrupt 用于阻塞当前线程
            // 线程是在这个方法里面阻塞的,醒来的时候仍然在for循环里面,就能再次自旋尝试获得锁
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获得node的锁失败,将 node 从队列中移除
        if (failed)
            cancelAcquire(node);
    }
}

方法的自旋过程在每一次循环开始时都会有一步判断p == head,满足这一条件通常有两种情况,

  1. node 之前没有获得锁,addWaiter方法中调用enq方法初始化同步队列后将其添加到了新创建的head节点之后。
    此时进入 acquireQueued 方法时,才发现当前节点的前置节点就是头节点,于是尝试获得一次锁
  2. node节点之前一直在阻塞沉睡,然后被唤醒(唤醒操作从head节点开始)。此时唤醒 node 的节点正是其前一个节点。

如果 tryAcquire 成功,就立马把自己设置成 head,把其前置节点移除(因为前置节点是之前的head,同步队列的头节点相当于一个没有用的空节点)。
如果 tryAcquire 失败,尝试进入同步队列。

acquireQueued方法的返回值进行说明,

  1. 返回true,说明节点是进入到同步队列之后阻塞了一段时间才被从队列中取出
  2. 返回false,说明节点进入了同步队列,但是没等到被阻塞就被从队列中取出,即没有执行到shouldParkAfterFailedAcquireparkAndCheckInterrupt方法
setHead方法

将节点设置为同步队列的头节点,

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

同步队列的头节点相当于一个空节点,其作用只是指向其下一个节点。

shouldParkAfterFailedAcquire方法

该方法用于将当前的Node对象的前置节点waitStatus设置为SIGNAL前置节点的状态为SIGNAL时,当前节点就可以阻塞

// 参数是当前节点的前置节点和当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前置节点状态已经是SIGNALED,说明当前节点不是新入队的节点
    // 该线程可以安全的被park阻塞
    if (ws == Node.SIGNAL)	
        return true;
    // 前置节点是CANCELLED状态说明该节点无效,将当前节点挂到更前面的有效节点之后    
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {	
    // 前置节点状态是0或PROPAGATE说明当前节点是新入队的节点
    // 需要将前置节点的状态改为SIGNAL,但是不立刻对其执行park操作。而是返回false,回到自旋过程中在新一轮循环中才阻塞
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire方法返回值的说明,

  • 返回 true,前置节点状态已经是SIGNALED,当前节点的线程可以被安全阻塞
  • 返回 false,前置节点状态刚刚被调整为SIGNALED,当前节点的线程不立刻阻塞。而是回到acquireQueued方法的自旋过程中在新一轮循环中阻塞
parkAndCheckInterrupt方法

该方法用于阻塞线程,线程是在这个方法里面阻塞的,醒来的时候仍然在acquireQueued方法的 for 循环里面,就能再次自旋尝试获得锁,

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

获取共享锁—acquireShared方法

acquireShared 整体流程和 acquire 相同,代码也很相似,贴出来不一样的代码进行比较,

1)尝试获取锁—tryAcquireShared方法

image

2)setHeadAndPropagate方法

第二步不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(setHead 方法)。但如果是共享锁的话,还会去唤醒自己的后续节点,一起来获得该锁(setHeadAndPropagate 方法)。

不同之处如下(左边排它锁,右边共享锁),
image
setHeadAndPropagate方法源码如下,

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);
    // propagate > 0 表示已经有节点获得共享锁了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //共享模式,还唤醒头节点的后置节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

// 释放后置共享节点
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 还没有到队尾,此时队列中至少有两个节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果队列状态是 SIGNAL ,说明后续节点都需要唤醒
            if (ws == Node.SIGNAL) {
                // CAS 保证只有一个节点可以运行唤醒的操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                // 进行唤醒操作
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }
        
        if (h == head)   
            break;
    }
}

总结

本节的重点是获取排它锁的acquire方法,整体的流程图如下,
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值