AQS锁和同步器框架

AQS

墙裂推荐:Java并发之AQS详解
https://www.cnblogs.com/waterystone/p/4920797.html

AbstractQueuedSynchronizer, 一个用来构建锁和同步器的框架。

ReentrantLock,Semaphore, FutureTask都是基于AQS来构建的。

img

image-20220323221635887

image-20220324135111907

独占模式线程acquire流程

此方法是独占模式下的线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,知道获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。

源码:

// AQS
public abstract class AbstractQueuedSynchronizer{
    
    public final void acquire(int arg) {
        // 1. 尝试获取资源
        if (!tryAcquire(arg) &&
            // 2. 将线程加入等待队列尾部,并标记为独占模式
            // 3. 是线程阻塞在等待队列中获取资源。
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 4. 等待过程中若出现中断,在获取资源后再进行中断处理。
            selfInterrupt();
    }
    
    // 1.尝试获取独占资源。
    // 具体的资源获取、释放方式交给自定义的同步器去实现。
    // 非abstract方法,独占模式只需要实现tryAcquire和tryRelease。共享模式只需要实现tryAcquireShared和tryReleaseShared。减少了不必要的实现。
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 2.加入等待队列
	private Node addWaiter(Node mode) {
        // 将线程封装为节点。mode为EXCLUSIVE 或 SHARED
        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;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 队列中没有元素,通过enq入队
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        // CAS自旋等待,直到成功加入队尾
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 队列为空则创建一个空的结点作为head,将node插入在head之后。
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 【指向前一个】
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    // 3.加入等待队列中的线程进入休息(停放)状态,直到其它线程释放资源后通知自己。
    final boolean acquireQueued(final Node node, int arg) {
        // 未拿到资源
        boolean failed = true;
        try {
            // 等待过程中是否被中断
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 前驱结点
                final Node p = node.predecessor();
                // 前驱为head,该结点可获取资源。且已经获取到资源。
                // head要么为null,要么为当前获取到资源的那个结点。
                if (p == head && tryAcquire(arg)) {
                    // 将node置为head结点的状态。prev=null。
                    setHead(node);
                    // prev和next都为null了,方便GC回收。意味着拿过资源的结点出队。
                    p.next = null; // help GC
                    // 已拿到资源
                    failed = false;
                    // 返回等待过程中是否被中断
                    return interrupted;
                }
                // 如果自己可以休息,通过park()进入waiting状态,直到被unpark().
                // 如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
                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;
    }
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 拿到前驱状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 该节点已经设置了状态,要求释放时发出信号,因此后继结点可以安全地停放。
            return true;
        if (ws > 0) {
            // 前驱结点是取消状态(>0),跳过此结点,继续向前寻找。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 找到一个正常的前驱结点。
            pred.next = node;
        } else {
            // 前驱正常,将其设为signall,释放资源后通知自己。有可能失败,也许前驱结点刚释放完。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private final boolean parkAndCheckInterrupt() {
        // 调用park方法,使线程进入waiting状态。
        // 等待unpack()方法或者interrupt()唤醒自己。
        LockSupport.park(this);
        // 如果被唤醒,查看自己是不是被中断的。
        return Thread.interrupted();
    }
    
    // 4. 中断方法
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
}

流程总结:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

img

独占模式线程release流程

此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(state=0),它会唤醒等待里的其它线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。

源码:

public abstract class AbstractQueuedSynchronizer{
    
    public final boolean release(int arg) {
        // 1.释放资源
        // 根据tryRelease的返回值来判断线程是否已经完成资源的释放。
    	// 在自定义同步器在设计tryRelease时候需要注意该返回值。
        if (tryRelease(arg)) {
            // 2.找到头结点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 3.唤醒等待队列里的下一个线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    // 1. 释放资源,需要在自定义的同步器中去实现。
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 2. 当前结点置为0状态,唤醒等待队列中的下一个线程
    private void unparkSuccessor(Node node) {
        // node为当前线程,状态为负数,则需要清除等待信号。
        int ws = node.waitStatus;
        // 小于0则状态置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)
                // 小于等于0的都是有效的结点
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 唤醒上述过程中找到的下一个有效线程
            LockSupport.unpark(s.thread);
    }
    
    
}

为什么要从后往前找?

有两种异常情况,会导致next链不一致:

1)s==null,在新结点入队时可能会出现

img

2)s.waitStatus > 0,中间有节点取消时会出现(如超时)

img

总之,由于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。

为什么prev链是强一致的?因为addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。这里的组合用法非常巧妙,能保证CAS之前的prev链强一致,但不能保证CAS后的next链强一致。

共享模式线程acquireShared流程

此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

public abstract class AbstractQueuedSynchronizer{
    
    public final void acquireShared(int arg) {
        // 1. 获取资源
        if (tryAcquireShared(arg) < 0)
            // 2.将当前线程加入队列尾部
            doAcquireShared(arg);
    }
    
    // 1. 尝试获取资源
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 2.将当前线程加入队列尾部
    private void doAcquireShared(int arg) {
        // 加入队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 前驱
                final Node p = node.predecessor();
                // 如果前一个是head,当前线程有被唤醒有可能是head用完资源来唤醒自己的
                if (p == head) {
                    // 尝试获取资源
                    int r = tryAcquireShared(arg);
                    // 获取成功
                    if (r >= 0) {
                        // 将head指向自己
                        setHeadAndPropagate(node, r);
                        // prev和next都设置为null了
                        p.next = null; // help GC
                        if (interrupted)
                            // 等待过程被中断过,此时才执行中断流程
                            selfInterrupt();
                        // 已获取资源
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        // 如果还有剩余量,继续唤醒下一个有效线程
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
}
    1. tryAcquireShared()尝试获取资源,成功则直接返回;
    2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。

其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)

共享模式线程releaseShared流程

public abstract class AbstractQueuedSynchronizer{
    
	public final boolean releaseShared(int arg) {
        // 1. 尝试释放资源
        if (tryReleaseShared(arg)) {
            // 2. 唤醒后续结点
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    private void doReleaseShared() {
        
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒后继
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

    
}

此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。

acquire()和acquireShared()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,相应的源码跟acquire()和acquireShared()差不多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值