AQS源码分析(上)

本文深入解析AQS(AbstractQueuedSynchronizer)的内部机制与ReentrantLock的源码实现,阐述了AQS如何通过双向链表实现FIFO队列,以及ReentrantLock如何基于AQS实现公平锁与非公平锁。

AQS源码分析–前置知识准备

AQS(AbstractQueuedSynchronizer):抽象式队列同步器
AQS 是一个FIFO的队列,由双向链表实现,具有内部类Node。它定义了多线程访问共享资源的同步器框架,许多同步类实现都依赖于它。
这里通过ReentrantLock来介绍AQS的底层源码时如何实现的,以下为一个简单的例子:

class Counter{
    private final ReentrantLock lock = new ReentrantLock(true);
    private int count;
    public void add(int n) {
        lock.lock();
        try {
            count += n;
            System.out.println(count);
        } finally {
            lock.unlock();
        }
    }
}

AQS内部重要成员介绍

在走ReentrantLock源码之前,先看一下AQS内部的成员变量和成员方法,方便之后的解读

//内部类Node
static final class Node {
    static final Node SHARED = new Node();  //共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
    static final Node EXCLUSIVE = null;  //独占,只有一个线程能执行,如ReentrantLock
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;  //休眠状态,等待被唤醒
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus;  //等待状态,有1,0(初始化状态),-1,-2,-3五个值
    volatile Node prev;  //指向前节点
    volatile Node next;   //指向后节点
    volatile Thread thread;  //用于指向等待锁的线程对象
    Node nextWaiter;
    final Node predecessor() throws NullPointerException { //返回前节点
        Node p = prev;
         if (p == null)
             throw new NullPointerException();
         else
             return p;
    }
    Node() {    
    }
    Node(Thread thread, Node mode) {  
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { 
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}
**//AQS通过head,tail,内部类-Node实现一个FIFO的队列(也是一个双向的链表)**
private transient volatile Node head; //head指向队列的头部Node
private transient volatile Node tail; //tail指向队列的尾部Node
private volatile int state;  //共享资源--锁状态,加锁成功则为1,重入+1,释放锁为0
protected final int getState() {return state;}
protected final void setState(int newState) {state = newState;}
public final boolean hasQueuedPredecessors() {  //判断队列中是否有节点
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
...    //其余方法等ReentrantLock源码中分析到的时候再说

ReentrantLock内部结构

在这里插入图片描述ReentrantLock源码中提供了两个构造器方法,分别用来构造公平锁和非公平锁,并用父类变量sync接收。

private final Sync sync;  //ReentrantLock的一个字段,可以指向Sync对象或子类(NonfairSync/FairSync)对象
非公平锁:
public ReentrantLock() {
    sync = new NonfairSync();
}
公平锁:
public ReentrantLock(boolean fair) {  
    //fair传参为true,则构建公平锁;否则为非公平锁
    sync = fair ? new FairSync() : new NonfairSync();  
}

公平锁–加锁

以公平锁的加锁为例:

//ReentrantLock的lock方法
public void lock() {
     sync.lock();
}
//跳到Sync抽象类的抽象方法
abstract void lock();
//Ctrl+Alt+B一下,发现调用的是FairSync中的lock方法
final void lock() {
    acquire(1);
}
//调用AQS的acquire方法  
//到这一步,tryAcquire,addWaiter,acquireQueued三个方法极为重要,我们分开来解读
public final void acquire(int arg) {  //arg=1
   //tryAcquire方法:
   //1. 返回false,表示未获取到锁,取反后为true,之后执行addWaiter方法,该方法用来把为获取到的线程放入等待队列中
   //2. 返回true,表示获取锁成功,取反后为false,则跳出判断,执行完毕,最后跳出lock()方法,继续执行lock之后的代码
    if (!tryAcquire(arg) &&  
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire方法

FairSync的tryAcquire方法:顾名思义,该方法用来尝试获取锁

protected final boolean tryAcquire(int acquires) {  //acquires=1
    final Thread current = Thread.currentThread();  //获取到当前线程
    int c = getState();  //AQS的方法:获取state
    if (c == 0) { //如果锁状态为0,即锁未被获取  注:第一个线程尝试获取锁的时候,c必然等于0
     //!hasQueuedPredecessors()--判断当前线程之前是否有其他线程排队,即是否有资格去获取锁(因为这是公平锁,先来的先获取)
     //compareAndSetState--尝试获取锁,即尝试把state的状态从0改为1。如果当前线程去获取锁的同时,也有其他线程去获取锁。
     //1. 如果获取成功,则执行setExclusiveOwnerThread方法,表示当前线程获取了该锁
     //2. 如果获取失败,则return false
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果state!=0,判断是是否是持有锁的线程在重入锁
    else if (current == getExclusiveOwnerThread()) {  //如果当前线程为持有锁的线程,则进行重入锁操作
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);  //将AQS中的state修改,加1
        return true;
    }
    return false;
}

hasQueuedPredecessors----判断队列是否有等待的节点----自认为是这段代码是精髓之一

public final boolean hasQueuedPredecessors() {
        Node t = tail;  //尾节点
        Node h = head;  //头节点
        Node s;
        return h != t &&  
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
h!=t:
 1. 如果h等于t,说明此时队列未初始化(head和tail都为null)或者队列中没有等待的节点(即只有哨兵节点)
 2. 如果h不等于t,说明队列中有等待的线程节点,head指向哨兵节点,tail指向等待的节点。故此h!=t返回true
    2.1 继续先执行(s = h.next) == null:
结论:
  1. 当队列未初始化或者队列中无等待节点,返回false,表示没有比当前线程更早排队等待获取锁的线程,即当前线程不需要排队

AbstractOwnableSynchronizer的setExclusiveOwnerThread

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;  //表示获取独占锁的线程是哪个线程对象
}

在这里插入图片描述

注:以上成功获取锁的流程是比较凑巧的,如果当锁未被占用,某一个线程获取锁的时候,还未初始化队列,或者队列中没有等待的线程,且去获取锁的时候正好被这个线程给获得了。
但是要注意,在并发中,多个线程是会同时执行这个流程的,要站在一个动态的角度去解读并发编程。
所以可能存在以下情况,使得获取不到锁:
1. 锁已经被占用了(c!=0)
2. hasQueuedPredecessors返回false,说明此时队列未初始化或这队列中无等待的线程,之后进行compareAndSetState失败,即被其他线程抢先一步获取到了锁
3. hasQueuedPredecessors的时候返回true,说明有一个线程抢到了锁,并且至少有一个线程入队了。对于公平锁,后来的线程没有资格获取锁

以上的情况,使得tryAcquire返回false,取反后为true,会执行addWaiter方法,就是把当前线程放到队列中去

addWaiter方法–入队自旋

private Node addWaiter(Node mode) {  //mode--Node.EXCLUSIVE=null
    //这里初始化了一个Node对象,回到AQS内部重要成员介绍中可以知道,这里在Node对象内部用一个变量指向了当前线程对象
    Node node = new Node(Thread.currentThread(), mode); 
    Node pred = tail;  //尾节点
    if (pred != null) {  //这个判断表示是否已经初始化过队列
        node.prev = pred;  //将当前节点的prev指向原先的尾节点
        if (compareAndSetTail(pred, node)) {  //将tail指向当前节点
            pred.next = node;  //原先尾节点的next指向当前节点
            return node;
        }
    }
    enq(node);  //执行enq的情况:1. 初始化队列   2.compareAndSetTail未成功
    return node;
}

AQS的enq方法
1. 如果未初始化过队列则会执行enq方法,进行初始化队列操作
2. 如果addWaiter中的compareAndSetTail未成功,也会执行enq方法

private Node enq(final Node node) {
    for (;;) {  //死循环
        Node t = tail;
        if (t == null) {  //执行这里的代码说明当前的队列还未初始化
            //compareAndSetHead:
              //1. new Node():创建一个thread变量为null的节点,并将head指向该节点
              //2. tail也指向该节点
           //说明:队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化, 以提升性能
            if (compareAndSetHead(new Node()))  
                tail = head;  //同时将tail指向该节点
        } else {  //这里用于设置尾节点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;  //最终都在这里跳出死循环
            }
        }
    }
}
注:
1. enq方法中的compareAndSetHead操作可能失败:当多个线程同时在还未初始化队列的时候执行该CAS操作,都想要将head和tail指向这个thread为null的节点。
2. 但是在这里失败的线程,或者是成功的线程都将进入下一个循环。
3. 此时,队列已经被初始化了,每个线程都要执行compareAndSetTail方法,将自己这个线程节点放到尾节点。
4. compareAndSetTail失败的节点会继续进入下一个循环,执行compareAndSetTail,直到在某个时刻执行CAS成功,在(return t)处跳出死循环。

在这里插入图片描述
在这里插入图片描述多个线程节点同时入队时,会出现以下情况:
在这里插入图片描述

addWaiter总结:在这里插入图片描述

acquireQueued----线程在这里阻塞;自旋

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();  //获取当前节点的前节点
            //如果前节点为哨兵节点(即当前节点是第一个等待锁的线程;如果当前节点不是第一个等待锁的线程,没有资格去尝试获取锁)
            //且获取锁成功(可能当前线程入队后,锁正好被释放了,所以在入队后自旋tryAcquire,尝试获取锁)
            if (p == head && tryAcquire(arg)) { 
                setHead(node); //这里将head指向了获取到锁的node节点,这个节点的thread被置为null,成为哨兵节点
                p.next = null;   //原先的哨兵节点出队列
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private void setHead(Node node) {  //将节点设置为头节点(哨兵节点)
    head = node;
    node.thread = null;
    node.prev = null;
}

shouldParkAfterFailedAcquire–获取锁失败后判断是否应该park

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //pred--前节点  node--当前节点
    int ws = pred.waitStatus;  //前节点的等待状态
    if (ws == Node.SIGNAL)  //如果前节点的waitStatus已经是-1,即等待唤醒的状态,则return true,然后执行parkAndCheckInterrupt睡眠
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  //将前节点的ws赋值为-1,表示等待唤醒状态
    }
    return false;
}

parkAndCheckInterrupt----在这里睡眠

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

重回hasQueuedPredecessors
在这里插入图片描述

### 关于 AQS (AbstractQueuedSynchronizer) 源码分析 #### AQS 基本概念 AQS 是 `java.util.concurrent` 包下用于构建锁和其他同步组件的基础框架[^1]。作为一个抽象类,AQS 提供了一种机制来管理线程之间的排队和阻塞/唤醒操作。 #### 核心成员变量 - **exclusiveOwnerThread**: 继承自父类 `AbstractOwnableSynchronizer`,表示当前持有独占锁的线程对象[^3]。 此字段有助于追踪哪个线程正在占用资源,在调试或监控时非常有用。 #### 同步队列结构 AQS 使用 FIFO 队列来维护等待获取锁或其他同步状态的线程列表。当一个线程尝试获取某个资源失败时会被加入到这个队列中,并进入休眠直到被其他线程唤醒。 #### 主要方法解析 为了支持不同的同步需求,AQS 定义了一系列模板方法让子类去实现: - `tryAcquire(int arg)` 和 `tryRelease(int arg)` 这两个方法由具体的同步器实现,用来控制如何获取和释放共享资源的状态变化逻辑[^5]。 - `isHeldExclusively()` 判断当前线程是否已经持有了独占式的锁。 此外还有多个辅助性的内部方法如 `acquireQueued(Node node, int arg)` 来处理实际的入队、出队以及线程调度工作。 #### LockSupport 工具类的作用 在 AQS 实现过程中大量使用到了 `LockSupport.park()` 及其变体函数来进行线程挂起与恢复的操作[^4]。这些 API 能够高效地使线程暂停执行并响应中断信号,从而实现了轻量级且高性能的线程间通信方式。 ```java // 示例代码展示 park 方法调用场景 public final void acquireInterruptibly(int arg) throws InterruptedException { if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } 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()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } ``` 通过上述介绍可以看出,AQS 不仅是一个强大的底层设施,而且设计精巧合理,能够很好地满足各种复杂并发环境下的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值