分析juc中AQS阻塞唤醒机制

这里以ReentrantLock为切入点分析AQS实现多个线程上锁以及如何阻塞等待的分析

ReentrantLock中实现公平锁和非公平锁是用两个类实现的:FairSyncNonfairSync

AQS指的是AbstractQueuedSynchronizerReentrantLock里面的抽象静态内部类Sync继承自AQS,而ReentrantLock实现公平锁和非公平锁的两个类则是继承自Sync

  • image-20221103171418397

1.AQS同步队列结构

  • image-20221103175056016

    • 这里面的NodeAQS的一个内部类,每个Node表示一个在同步队列中等待抢锁的线程,他们一起形成了一个双端队列,每个Node都有prevnext属性指向前后Node

      • 注意:该队列第一个是虚拟结点,从第二个结点开始才是代表不同的线程

        • 队列为空的时候是这么设置的:

          if (compareAndSetHead(new Node()))
              tail = head;//头尾指针都指向虚拟头结点
          
          • new一个空结点当头结点,头尾指针都指向虚拟头结点
          • 头尾指针相同的情况就是此时这样只有一个虚拟头结点,或者队列未初始化,都是null
      • 即抢锁未成功的线程需要进入该队列阻塞等待

      • image-20221103175333066

      • image-20221103175403435

2.ReentrantLock上锁方法lock()

  1. 公平锁:

    final void lock() {
        //直接尝试获得,这个1表示让锁状态计数器+1,这个状态是0表示锁没人占用,>0表示有人使用,一般一个线程用lock()方法获得锁让该计数器从0变成1,unlock()一次就-1,变成0就表示锁没人占用。但是ReentrantLock是可重入锁,一个线程如果在释放锁前再次进入,就会让该计数器从1加到2,这样他就需要用unlock()释放两次锁
        acquire(1);
    }
    
  2. 非公平锁

    final void lock() {
        //非公平锁直接尝试抢夺锁,compareAndSetState(0, 1)表示如果锁的状态计数器是0,则将它变成1,即该锁进入被占用状态
        if (compareAndSetState(0, 1))
            //如果修改锁状态成功,将当前线程设置成锁的拥有者
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //如果抢夺失败,和公平锁一样进入acquire()
            acquire(1);
    }
    

acquire()方法

//arg就是上面传入的1
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 该方法是AQS类中的方法,final修饰让子类无法覆盖,它包含三个关键方法:
    • tryAcquire(),acquireQueued(),addWaiter()
    • 其中tryAcquire()需要子类进行重写,另外两个不需要,这里使用的是设计模式中的模板设计模式
  • 其中acquireQueued()方法返回的结果是当前线程是否被中断,中断表示一个状态表示,如果是,这里进入到selfInterrupt()里面再次将当前线程中断,因为里面返回是否中断的时候用的是静态的Thread.intrrupt(),这个方法除了返回中断标识,还会清空中断状态。
    • 我认为用这样是为了让线程在acquireQueued()里继续运行完,因此将中断标识位清空,将结果返回,最后在这里自己主动中断。

2.1 tryAcquire()方法

  • 该方法需要子类重写,因此公平锁和非公平锁实现不一样,但是只有一个地方不同:

    • 公平锁:

      protected final boolean tryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          int c = getState();
          if (c == 0) {
              if (!hasQueuedPredecessors() &&
                  compareAndSetState(0, acquires)) {
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          else if (current == getExclusiveOwnerThread()) {
              int nextc = c + acquires;
              if (nextc < 0)
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
          return false;
      }
      
    • 非公平锁:

      protected final boolean tryAcquire(int acquires) {
          return nonfairTryAcquire(acquires);
      }
      final boolean nonfairTryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          //得到当前锁的状态
          int c = getState();
          //如果c==0表示锁没人占用
          if (c == 0) {
              //如果当前锁状态是0,就替换成acquires,抢到该锁
              if (compareAndSetState(0, acquires)) {
                  //然后将该锁的拥有线程设置成自己,然后返回true
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          //如果c!=0表示当前锁被占用,检查是否是自己占用的,是的话就让锁的状态计数器+1,表示重入一次
          else if (current == getExclusiveOwnerThread()) {
              int nextc = c + acquires;
              if (nextc < 0) // overflow
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
          //都不行就返回false,占用锁失败
          return false;
      }
      
  • 可以看到两个方法的唯一区别是这条判断逻辑公平锁多了!hasQueuedPredecessors()

hasQueuedPredecessors()
if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}
  • hasQueuedPredecessors()方法:

    • 该方法表示当前结点前面还有没有结点,因为公平锁需要公平争夺,如果同步队列(第一节图)中当前结点前面还有其他节点,则需要让他先争夺锁
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    
    • t指的是同步队列的尾结点,h指的是头结点
    • h!=t:如果两者相同,则直接返回false,外层获取它的结果的时候有取反!hasQueuedPredecessors(),因此最后是ture,表示可以争夺锁
      • 因为头尾指针相同的话表示:
        1. 队列还未初始化,此时h==t==null,此时队列中没有其他节点,可以争夺锁
        2. 队列已经初始化,但是只有一个虚拟头结点(第一节),此时h==t,可以争夺锁
    • (s = h.next) == null || s.thread != Thread.currentThread(),如果h.next==null表示当前前队列只有除了虚拟头结点没有其他结点,此时h==t,不会走到这里,因此走到这里判断的时候h.next一定不为空
    • 然后s.thread != Thread.currentThread()判断当前线程是否是第一个线程h.next(h是虚拟头结点,真正的线程结点是第二个h.next),如果相等,返回false,经过外层取反是true,表示可以争夺锁

2.2 addWaiter()方法

private Node addWaiter(Node mode) {
    //形成当前线程的结点
    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属性指向尾结点
        node.prev = pred;
        //将尾结点设置成当前结点
        if (compareAndSetTail(pred, node)) {
            //将pred指向的之前的尾结点的next属性指向当前结点
            pred.next = node;
            return node;//返回,因此一般不会进入下面的enq(node)
        }
    }
    //一般是上面pred==null的时候进入,表示队列未初始化
    enq(node);
    return node;
}
  • enq(node)

    private Node enq(final Node node) {
        for (;;) {//死循环
            Node t = tail;
            //尾结点为空,表示需要初始化
            if (t == null) { // Must initialize
                //将空的Node设置成头结点head
                if (compareAndSetHead(new Node()))
                    tail = head;//将head赋给tail
            } else {
                //第一轮循环初始化队列,第二轮进来这里
                node.prev = t;//记录此时尾结点是哪一个
                //将当前node设置成新的尾结点
                if (compareAndSetTail(t, node)) {
                    //将之前的尾结点的next指向node
                    t.next = node;
                    return t;//退出死循环
                }
            }
        }
    }
    
    • 可以看到enq()是用来初始化队列的,它是死循环,第一轮循环初始化队列,设置虚拟头结点,第二轮循环就是把当前结点入队,这部分代码其实和进入enq()之前判断已经初始化后执行的是一样的

      • //如果队列已经初始化,则不会进入enq
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //===========================
        //enq中初始化队列后
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
        

2.3 acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//表示是否取消排队
    try {
        boolean interrupted = false;//是否被中断
        for (;;) {//死循环,除非取消排队或者抢到了锁,不会退出?
            
            final Node p = node.predecessor();//得到当前结点的前驱结点
            //如果p是头结点,就又用tryAcquire尝试抢锁,因为此时当前结点是第一个线程,无论公平还是非公平自己都能抢夺
            if (p == head && tryAcquire(arg)) {
                //抢锁成功,需要将结点出队,这里采用的方法是将当前结点当做下一轮的虚拟头结点,让原本的头结点p置空被GC回收,代码在底下
                setHead(node);
                p.next = null; // help GC
                failed = false;//不取消排队
                return interrupted;//未被中断false
            }
            //shouldParkAfterFailedAcquire方法下面单独分析
            if (shouldParkAfterFailedAcquire(p, node) &&
                //前面返回true表示当前线程进入等待资源释放状态,因此需要阻塞,该方法直接将线程阻塞,代码在底下
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)//最后看是否想取消排队
            cancelAcquire(node);
    }
}
//===================================
//setHead方法
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
//parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//直接阻塞当前线程,用的是通行证阻塞
    return Thread.interrupted();
}
    • 可以看到死循环中只有一个出口就是return,也就是说没抢到锁就不会退出,但也不一定一直循环抢,后面会看当前线程的等待状态判断是否阻塞,阻塞后就等待其他抢到锁的线程退出的时候需要取消排队,cancelAcquire(node)就有唤醒机制,抢到后将排队意愿设置成取消,后面的方法会将当前线程对应的结点从同步队列中移除(就像链表移除结点,将前驱或后继连接)
  • 注意:parkAndCheckInterrupt()方法结束阻塞后调用静态方法判断当前线程是否被中断,静态方法会清楚当前中断标志位,因此无论是否被终端,返回到上一层

  • shouldParkAfterFailedAcquire()

    • image-20221103190538946
      • 可以看到每次都是将自己的前驱结点设置成**等待被占用资源释放**状态,即SIGNAL

2.4 cancelAcquire()方法

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;//将当前结点表示的线程置空

    // Skip cancelled predecessors
    Node pred = node.prev;//得到当前结点前驱
    //如果前驱也取消了,就像自己的左结点指向前驱的前驱,一直到找到未取消的前驱
    while (pred.waitStatus > 0)//如果前驱状态为>0(只有处于取消状态的状态为是大于0的),就跳过,找前驱的前驱
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;//当前结点状态置为取消状态

    // If we are the tail, remove ourselves.
    //如果当前节点是尾结点,就将前驱结点设置成新的尾结点,并且将pred的next置空
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        //总之就是如果前驱结点不是头结点,并且里面线程不为空,就将前驱结点的next指向当前结点的next
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;//拿出当前结点的next
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {//否则唤醒当前结点的后继结点,代码在下面
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
  • unparkSuccessor()

    private void unparkSuccessor(Node node) {
            /*
             * If status is negative (i.e., possibly needing signal) try
             * to clear in anticipation of signalling.  It is OK if this
             * fails or if status is changed by waiting thread.
             */
            int ws = node.waitStatus;
            if (ws < 0)
                compareAndSetWaitStatus(node, ws, 0);
    
            /*
             * Thread to unpark is held in successor, which is normally
             * just the next node.  But if cancelled or apparently null,
             * traverse backwards from tail to find the actual
             * non-cancelled successor.
             */
            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);
        }
    

3.ReentrantLock解锁方法unlock()

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {//锁释放成功返回ture,代码在下面
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //由于锁释放了,唤醒下一个结点让他从上面的阻塞醒来,争夺锁
            unparkSuccessor(h);
        return true;
    }
    return false;//没有成功释放锁
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//将锁状态计数器-1,如果没有重入的话c是1,此时-1等于0,表示锁释放了
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);//将锁的占用现成置为空
    }
    setState(c);//将锁状态更新
    return free;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一酒。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值