AQS源码

AbstractQueuedSynchronizer底层已经通过AQS队列实现了线程的阻塞和唤醒机制
分别对应下面函数,且均为final修饰,即子类不可修改
独占锁:acquire release
共享锁:acquireShared releaseShared

子类子需要继承AbstractQueuedSynchronizer并重写tryAcquire tryRelease tryAcquireShared tryReleaseShared等函数

以可重入锁的非公平锁为例

结点状态waitStatus

这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

获取锁

acqurie函数

// AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果线程被中断了逻辑来到这,完成一次真正的打断效果
        selfInterrupt();
}

tryAcquire

tryAcquire 尝试获取锁失败时, 会调用 addWaiter 将当前线程封装成node入队,acquireQueued 阻塞当前线程,
acquireQueued 返回 true 表示阻塞过程中线程被中断唤醒过,false 表示未被中断过。

tryAcquire :由子类自定义实现(可以实现公平锁和非公平锁)
ReentrantLock非公平锁为例:

// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// 抢占成功返回 true,抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // state 值
    int c = getState();
    // 条件成立说明当前处于【无锁状态】
    if (c == 0) {
        //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁        
    	if (compareAndSetState(0, acquires)) {
            // 获取锁成功设置当前线程为独占锁线程。
            setExclusiveOwnerThread(current);
            return true;
         }    
	} 
    // 这部分是重入锁的原理    
   	// 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】
	else if (current == getExclusiveOwnerThread()) {
        // 更新锁重入的值
        int nextc = c + acquires;
        // 越界判断,当重入的深度很深时,会导致 nextc < 0,int值达到最大之后再 + 1 变负数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内
        setState(nextc);
        return true;
    }
    // 获取失败
    return false;
}

正是这个方法体现了非公平锁,在nonfairTryAcquire如果发现state=0,无锁的情况,它会忽略队列中等待的线程,优先获取一次锁,相当于"插队"。

addWaiter

addWaiter将当前线程关联到一个 Node 对象上, 并加入阻塞队列

// AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点
private Node addWaiter(Node mode) {
    // 将当前线程关联到一个 Node 对象上, 模式为独占模式   
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 快速入队,如果 tail 不为 null,说明存在阻塞队列
    if (pred != null) {
        // 将当前节点的前驱节点指向 尾节点
        node.prev = pred;
        // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】
        if (compareAndSetTail(pred, node)) {
            pred.next = node;// 双向链表
            return node;
        }
    }
    // 初始时队列为空,或者 CAS 失败进入这里
    enq(node);
    return node;
}

enq函数:初始时队列为空,或者 CAS 失败后调用该方法自旋入队

// AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
    // 自旋入队,必须入队成功才结束循环
    for (;;) {
        Node t = tail;
        // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】
        if (t == null) {
            // 设置一个【哑元节点】,头尾指针都指向该节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 自旋到这,普通入队方式,首先赋值尾节点的前驱节点【尾插法】
            node.prev = t;
            // 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】
            if (compareAndSetTail(t, node)) {
                //【此时 t.next  = null,并且这里已经 CAS 结束,线程并不是安全的】
                t.next = node;
                return t;	// 返回当前 node 的前驱节点
            }
        }
    }
}

head结点不参与锁竞争,只负责唤醒后续结点,初始状态waitStatus 为0,后续有节点加入会被修改为-1,即signal
waitStatus 状态:0 为默认正常状态, -1状态表示它肩负唤醒下一个节点的线程。


acquireQueued

acquireQueued 会在一个自旋中不断尝试获得锁,失败且找到能唤醒自己的前驱结点后进入 park 阻塞等待被唤醒。

如果当前线程是在 head 节点后,也就是第二个节点,又会直接多一次机会 tryAcquire 尝试获取锁,如果还是被占用,失败再次进入park等待被唤醒。

线程结点

inal boolean acquireQueued(final Node node, int arg) {
    // true 表示当前线程抢占锁失败,false 表示成功
    boolean failed = true;
    try {
        // 中断标记,表示当前线程是否被中断
        boolean interrupted = false;
        for (;;) {
            // 获得当前线程节点的前驱节点
            final Node p = node.predecessor();
            // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
            if (p == head && tryAcquire(arg)) {
                // 获取成功, 设置当前线程自己的 node 为 head
                setHead(node);
                p.next = null; // help GC
                // 表示抢占锁成功
                failed = false;
                // 返回当前线程是否被中断
                return interrupted;
            }
            // 判断是否应当 park,返回 false 后需要新一轮的循环,返回 true 进入条件二阻塞线程
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                // 条件二返回结果是当前线程是否被打断,没有被打断返回 false 不进入这里的逻辑
                // 【就算被打断了,也会继续循环,并不会返回】
                interrupted = true;
        }
    } finally {
        // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
        if (failed)
            cancelAcquire(node);
    }
}

cancelAcquire

cancelAcquire
如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED

private void cancelAcquire(Node node) {
  // 将无效节点过滤
	if (node == null)
		return;
  // 设置该节点不关联任何线程,也就是虚节点
	node.thread = null;
	Node pred = node.prev;
  // 通过前驱节点,跳过取消状态的node
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;
  // 获取过滤后的前驱节点的后继节点
	Node predNext = pred.next;
  // 把当前node的状态设置为CANCELLED
	node.waitStatus = Node.CANCELLED;
  // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
	if (node == tail && compareAndSetTail(node, pred)) {
		compareAndSetNext(pred, predNext, null);
	} else {
		int ws;
    // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
    // 如果1和2中有一个为true,再判断当前节点的线程是否为null
    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
		if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				compareAndSetNext(pred, predNext, next);
		} else {
      // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
			unparkSuccessor(node);
		}
		node.next = node; // help GC
	}
}

上面的长if判断原理: 如果当前节点取消了,那是不是要把当前节点的前驱节点指向当前节点的后继节点,但是我们之前也说了,要唤醒或阻塞结点,须在其前驱节点的状态为 SIGNAL 的条件才能操作,所以在设置 pre 的 next 节点时要保证 pre 结点的状态为 SIGNAL,想通了这一点相信你不难理解以下代码。

1、首先第一步当前节点之前有取消结点时,则逻辑如下
在这里插入图片描述
2、如果当前结点既非头结点的后继结点,也非尾结点,即步骤 1 所示,则最终结果如下
在这里插入图片描述
这里肯定有人问,这种情况下当前节点与它的前驱结点无法被 GC 啊,还记得我们上文分析锁自旋时的处理吗,再看下以下代码

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 省略无关代码
    if (ws > 0) {
        // 移除取消状态的结点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } 
    return false;
}

这段代码会将 node 的 pre 指向之前 waitStatus 为非 CANCEL 的节点

所以当 T4 执行这段代码时,会变成如下情况
在这里插入图片描述
可以看到此时中间的两个 CANCEL 节点不可达了,会被 GC

3、如果当前结点为 tail 结点,则结果如下,这种情况下当前结点不可达,会被 GC
在这里插入图片描述
4、如果当前结点为 head 的后继结点时,如下
在这里插入图片描述
结果中的 CANCEL 结点同样会在 tail 结点自旋调用 shouldParkAfterFailedAcquire 后不可达,如下
在这里插入图片描述

shouldParkAfterFailedAcquire

寻找状态值为-1的前驱节点(能唤醒自己的前驱结点)
在进入park阻塞前,node结点要寻找能唤醒它的前驱结点,即waitStatus = -1的结点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 表示前置节点是个可以唤醒当前节点的节点,返回 true
    if (ws == Node.SIGNAL)
        return true;
    // 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 获取到非取消的节点,连接上当前节点
        pred.next = node;
    // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑
    } else {
        // 【设置上一个节点状态为 Node.SIGNAL】,返回外层循环重试
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回不应该 park,再次尝试一次
    return false;
}

shouldParkAfterFailedAcquire发现前驱节点等待状态是-1, 返回true,表示需要阻塞。
shouldParkAfterFailedAcquire发现前驱节点等待状态大于0,说明是无效节点,会进行清理。
shouldParkAfterFailedAcquire发现前驱节点等待状态等于0,将前驱 node 的 waitStatus 改为 -1,返回 false。

parkAndCheckInterrupt

private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
    LockSupport.park(this);
    // 判断当前线程是否被打断,清除打断标记
    return Thread.interrupted();
}

acquireQueued通过不断自旋尝试获取锁,即不断执行shouldParkAfterFailedAcquire,最终找到等待状态为-1的前驱节点,进行阻塞当前线程。parkAndCheckInterrupt通过调用LockSupport.park方法进行阻塞。

非公平锁和公平锁

非公平锁代码

// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// 抢占成功返回 true,抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // state 值
    int c = getState();
    // 条件成立说明当前处于【无锁状态】
    if (c == 0) {
        //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁        
    	if (compareAndSetState(0, acquires)) {
            // 获取锁成功设置当前线程为独占锁线程。
            setExclusiveOwnerThread(current);
            return true;
         }    
	} 
    // 这部分是重入锁的原理    
   	// 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】
	else if (current == getExclusiveOwnerThread()) {
        // 更新锁重入的值
        int nextc = c + acquires;
        // 越界判断,当重入的深度很深时,会导致 nextc < 0,int值达到最大之后再 + 1 变负数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内
        setState(nextc);
        return true;
    }
    // 获取失败
    return false;
}

公平锁代码

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }
 
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 锁重入
        return false;
    }
}
public final boolean hasQueuedPredecessors() {    
    Node t = tail;
    Node h = head;
    Node s;    
    // 头尾指向一个节点,链表为空,返回false
    return h != t &&
        // 头尾之间有节点,判断头节点的下一个是不是空
        // 不是空进入最后的判断,第二个节点的线程是否是本线程,不是返回 true,表示当前节点有前驱节点
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

与非公平锁最大的区别是:公平锁获取锁的时候先检查 AQS 队列中是否有非当前线程的等待节点,没有才去 CAS 竞争,有的话,就老老实实排队去吧。而非公平锁会尝试抢一次锁,如果抢不到的话,老老实实排队去吧。

锁释放

AbstractQueuedSynchronizer release函数
tryRelease(arg)释放锁成功则唤醒AQS队列中的一个线程

// AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
    // 尝试释放锁,tryRelease 返回 true 表示当前线程已经【完全释放锁,重入的释放了】
    if (tryRelease(arg)) {
        // 队列头节点
        Node h = head;
        // 头节点什么时候是空?没有发生锁竞争,没有竞争线程创建哑元节点
        // 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }    
    return false;
}

tryRelease需要子类自己实现:设置 exclusiveOwnerThread 为 null,state = 0
可重入锁,只有 state 减为 0, 才完全释放锁成功

// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
    // 减去释放的值,可能重入
    int c = getState() - releases;
    // 如果当前线程不是持有锁的线程直接报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否已经完全释放锁
    boolean free = false;
    // 支持锁重入, 只有 state 减为 0, 才完全释放锁成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 当前线程就是持有锁线程,所以可以直接更新锁,不需要使用 CAS
    setState(c);
    return free;
}

释放成功后唤醒AQS队列阻塞的线程,参数为head结点

private void unparkSuccessor(Node node) {
    // 当前节点的状态
    int ws = node.waitStatus;    
    if (ws < 0)        
        // 【尝试重置状态为 0】,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了
        compareAndSetWaitStatus(node, ws, 0);    
    // 找到需要 unpark 的节点,当前节点的下一个    
    Node s = node.next;    
    // 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        // AQS 队列【从后至前】找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 说明当前线程状态需要被唤醒
            if (t.waitStatus <= 0)
                // 置换引用
                s = t;
    }
    // 【找到合适的可以被唤醒的 node,则唤醒线程】
    if (s != null)
        LockSupport.unpark(s.thread);
}

为什么这里查找唤醒的节点是从后往前,而不是从前往后呢?

从后向前的唤醒的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失尾节点。

被唤醒的线程Thread-1会从 park 位置开始执行acquireQueued的自旋获取锁操作。
acquireQueued自旋获取锁部分代码

for (;;) {
            // 获得当前线程节点的前驱节点
            final Node p = node.predecessor();
            // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
            if (p == head && tryAcquire(arg)) {
                // 获取成功, 设置当前线程自己的 node 为 head
                setHead(node);
                p.next = null; // help GC
                // 表示抢占锁成功
                failed = false;
                // 返回当前线程是否被中断
                return interrupted;
            }

以非公平锁为例

  • 如果Thread-1加锁成功(没有竞争),设置exclusiveOwnerThread为Thread-1, state=1。
    head 指向刚刚成功获取锁线程所在的 Node,将原本的head清空等待被gc。
  • 另一种可能,突然来了Thread-2来竞争,体现非公平锁。如果Thread-2获取锁成功,Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值