AQS 同步框架学习笔记


学习地址:https://www.bilibili.com/video/BV12K411G7Fg?t=544.9

思路

同步管理框架设计思路

目标: CAS只能原子的修改内存上的一个值,然而实际的业务场景中,需要同步的资源却是以对象的形式进行封装,如何利用CAS的特性对对象资源进行同步

  • 通用性:下层实现透明的同步机制,与上层业务解耦
  • 利用CAS的原子性,修改共享标志位:如果标志位为空,则表示当前资源空闲,如果标志位部位空,则表示当前资源正在被使用,线程需要等待
  • 等待队列:阻碍其他线程的调用
  • 使用场景:
    • 线程1需要快速尝试一下获取共享资源,获取不到也没关系,会进行其他的逻辑处理。
    • 线程2则需要必须获取到共享资源才能进行下一步的处理,如果当前时刻没有获取到,可以进行等待。

细节实现

AQS成员变量

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head; // 队列的头节点

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail; // 队列的尾节点

    /**
     * The synchronization state.
     * 同步状态表示资源是否被占用的标志位,volatile保证线程之间的可见性
     * 在共享模式下,state还需要表示当前资源被多少个线程占用,所以使用int类型而不是boolean
     */
    private volatile int state; 
  • 线程获取锁的两种模式:独占和共享
    • 独占模式:一旦共享资源被线程占用,其他线程都必须等待(类似写锁)
    • 共享模式:共享资源被线程以共享模式占用,则其他共享模式的线程也可以使用(类似读锁)
  • 等待队列:如果一个线程在当前时刻没有获取到共享资源,可以选择进行排队。AQS的队列是一个FIFO(先进先出的双向链表)

队列中node的具体结构

    static final class Node {
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
        
        volatile int waitStatus; // 节点在队列中的等待状态,即上面的四种状态
        volatile Node prev; // 前指针
        volatile Node next; // 后指针
        volatile Thread thread; // 线程对象
	}

核心方法

  • 利用state和FIFO等待队列来管理多线程的同步状态从而满足同步的使用场景:
    • 线程1需要快速尝试一下获取共享资源,获取不到也没关系,会进行其他的逻辑处理。
    • 线程2则需要必须获取到共享资源才能进行下一步的处理,如果当前时刻没有获取到,可以进行等待。
tryAcquire 方法
	protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
  • 快速尝试一下获取共享资源,获取成功返回true,失败返回false
  • int值参数代表对state的修改,布尔返回值代表是否成功获取锁,protected 上层可以override这个方法,写自己的逻辑
acquire 方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • 此方法定能够获取到锁
  • public final 所有的继承类都直接调用这个方法,且不允许继承类擅自修改,这个方法一定能够获取到锁
  • addWaiter方法,将当前线程封装成一个node然后加入等待队列的队尾,返回值为当前节点
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)) {
                	// 获取成功后,当前节点会被设置为头节点,而之前的头结点出队
                    setHead(node); // 设置当前节点为头结点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果当前线程 需要被挂起,并且成功挂起,interrupted 才为true
                // parkAndCheckInterrupt是真正执行挂起的操作
                // 挂起成功之后,当前线程会阻塞等待被唤醒
                // 当当前节点成为头结点的next,则会被唤醒,则当前线程可以继续执行死循环,去尝试获取锁资源直到获取成功
                if (parkAndCheckInterrupt(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    // 此方法只是判断线程是否有被挂起的资格
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 当前节点状态为SIGNAL,则返回true,上层会真正执行挂起当前线程的操作
        if (ws == Node.SIGNAL) 
            return true;
        // ws > 0只有一种情况,node节点状态为1(CANCELLED),代表线程被取消,所以直接跳过该线程
        if (ws > 0) { 
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { 
        	// 如果代码执行到这里说明,node状态不是-1 SIGNAL也不是1(CANCELLED),只能是剩下两种,将其状态置为SIGNAL,返回false
        	// 上层的死循环会接着执行此方法,此时则会走前面的判断,返回true
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    // 真正挂起线程的方法
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);// park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。
        return Thread.interrupted(); // 设置线程的中断状态
    }
  • acquireQueued方法,配合release方法对线程进行挂起和响应,实现队列的先进先出
  • 在AQS的FIFO队列中,头结点是虚节点,头结点是占有共享资源的节点,第二个结点才是需要去获取锁的结点,当第二个结点获取到锁之后,头结点会出队。
tryRelease 方法
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
  • 快速尝试一下释放共享资源,释放成功返回true,失败返回false
release 方法
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    private void unparkSuccessor(Node node) { 
        int ws = node.waitStatus;
        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);
    }
  • 释放锁
  • 如果成功释放锁,则将锁标志位置为0,然后则需要唤醒下一个队列中的线程,让他去获取锁,从后往前找到队列中最前面的一个状态不是已删除的线程,唤醒他让他去自旋获取锁,当前head节点出列。

AQS学习总结

线程一定能获取到锁闭环的形成过程

  • 先尝试获取锁,能获取到则返回true,获取不到则将当前线程封装成node,加入等待队列插入队尾,并挂起当前线程
  • 等待队列的头结点使用完资源,释放锁之后会从尾节点开始向前寻找最前面一个可用的node节点,并唤醒node封装的线程
  • 线程一旦被唤醒,则会继续执行死循环来获取锁,成功获取到锁之后会经当前节点设置为头结点,之前的头结点出队
  • 闭环形成

等待队列

  • 所以一个等待队列中,只有头结点会占有锁,其他线程都处于被阻塞的状态,只有头结点即将释放锁时,第二个可用节点的线程会被唤醒然后执行死循环去获取锁资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值