AbstractQueuedSynchronizer深度学习(独占锁)

本文深入探讨了AbstractQueuedSynchronizer(AQS)在独占模式下的工作原理,包括其概念、状态同步、模板方法以及同步队列的管理。AQS作为Java并发编程的核心基础,用于实现并发控制,支持独占和共享两种模式。文章详细介绍了acquire()、tryAcquire()等关键方法,并阐述了如何通过FIFO队列管理线程竞争,以及在获取锁失败时如何将线程添加到队列中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

AbstractQueuedSynchronizer简称AQS,即抽象队列同步器。AQS是JAVA并发编程的核心基础类。所有的并发都是基于AQS进行扩展。AQS内部采用FIFO队列进行线程并发的管理。
类AQS采用模板方法进行设计,可以让更多的子类去扩展自己需要的功能。AQS主要提供两种模式并发模型:
1. 独占模式:任何时刻只能有一个线程获得当前执行锁定
2. 共享模式:任何时刻可以有多个线程获得当前执行锁定
本文主要讲述独占模式的锁定。

状态同步

AQS主要是通过如下3个方法进行状态同步,获取锁成功状态设置为1,释放锁状态设置为0:
1. getState()
2. setState(int expert)
3. compareAndSetState(int expert,int update)

模板方法

  1. acquire(int arg)
    1.1 该方法是独占模式下获取同步状态,如果获取锁成功,则直接返回。如果获取锁失败,则进入同步队列中,同时调用子类(NonFairSync和FairSync)中重写的tryAcquire方法。后续详细说明
  2. acquireInterruptliby
    2.1 该方法与acquire相似,但是该方法响应线程中断。如果线程进入同步队列中被打断,则该方法抛出异常。
  3. tryAcquireNanos
    3.1 该方法只是增加了超时限制,在规定 的时间内如果线程未获得同步状态,则返回false。获得同步状态则返回true。
  4. release()
    4.1 该方法为释放同步状态,待当前线程释放同步状态后,会将其后继节点中的线程唤醒。该方法参数1,最后调用LockSupport的unpark方法完成唤醒首节点等待的线程

重写方法

  1. tryAcquire
    1.1 需要子类去重写,在ReentrantLock中,具体的子类FairSync和NonFairSync重写tryAcquire方法。实现该方法需要查询当前状态并判断是否符合预期值,调用CAS进行设置。
  2. tryRelease
    2.1 该方法是独占模式下的释放同步状态。释放同步状态后会将后继节点中的线程唤醒。
  3. isHeldExclusively()
    3.1 该方法是监测当前锁定是否为当前线程

同步队列

  1. AQS(同步器)中的同步队列是基于FIFO设计,是一个双向链表。当线程获取同步状态失败后,会将当前线程,当前等待状态,前驱节点,后驱节点等信息封装成一个节点Node。加入到阻塞队列(FIFO)。
  2. 阻塞队列中的每一个节点都是封装了获取当前同步状态失败的线程引用,等待状态,前驱节点引用,后继节点引用。
    同步队列的结构图如下:
    这里写图片描述
  3. 根据上图可以看出,同步管理器内部其实就是一个双向链表。其中每一个节点本身都持有前驱节点后驱节点的引用,当前阻塞线程的引用当前线程在队列中的状态。同时队列的尾节点就是tail节点。
  4. 在AQS中,同步队列采用静态内部类方式实现,通过源码分析一下数据结构:
 static final class Node {
        //共享模式
        static final Node SHARED = new Node();
        // 独占模式
        static final Node EXCLUSIVE = null;
        /***线程状态waitStatus的各种状态值***/
        // 状态为1,状态取消,从FIFO去取消
        static final int CANCELLED =  1;
        // 表明当前节点的后继节点需要被唤醒,
        static final int SIGNAL    = -1;
        // 表明该节点的线程处于等待队列中,不在同步队列中。
        static final int CONDITION = -2;
        // 当前下一次共享模式下会将同步状态传播下去
        static final int PROPAGATE = -3;
        /**waitStauts=1:表明该线程由于超时或者被中断,需要从同步队列中取消。该节    
        **              点状态将不会再次发生变化。
        **waitStauts=-1:表明当前节点的后继节点(lockSupport的park方法阻塞)需要
        **              被唤醒,实际通过LockSupport的unpark唤醒。如果当前节点
        **              释放同步状态或被取消(状态为1),需要通知它的后继节点
        **waitStauts=2: 表明当前节点处于等待队列中,不是同步队列中。节点的线程等
        **              待在Condition上,需要调用Condition的singal方法将这个
        **              节点从等待队列转入到同步队列,同时状态设置为0?(不确定)             
        **waitStauts=3:表明下一次共享模式下将会传播该状态
        **waitStauts=0:初始状态,线程什么也不做。
        ** waitStatus状态值的改变都是通过CAS操作
        **/
        volatile int waitStatus;
        // 当前节点的引用prev指向前驱节点
        volatile Node prev;
        // 当前节点的引用next指向后继节点
        volatile Node next;
        // 当前节点持有的阻塞线程,FIFO中每个节点都是封装了一个阻塞线程
        volatile Thread thread;
        // 等待队列中的后继节点,需要调用Condition才能将其转入到同步队列中
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
       /**
       **  返回当前节点的前驱节点,代码显而易见。
       **/
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
       // Used to establish initial head or SHARED marker
        Node() {  }
         // 线程阻塞时,加入新的对尾节点需要调用该构造方法
         // thread:当前阻塞线程
        Node(Thread thread, Node mode) {    
            this.nextWaiter = mode;
            this.thread = thread;
        }
         // Used by Condition
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

添加结点

  1. 并发环境中发生获取锁等待时,JVM底层就会将当前阻塞的线程加入到同步队列中。再构造同步队列时,需要首先实例化head和tail节点。添加节点过程如下:
    • 添加节点1:第一次添加节点时,需要构造同步队列,前提就是要初始化head节点和tail节点。初始化head和tail节点时,二者指向同一个空节点(不持有当前阻塞线程的引用)。加入第一个节点时,将head.next=node1;node1.prev=head。同时,将tail引用指向node1,tail= node1,即tail指向node1。
    • 添加节点2:将node2加入到node1的尾部。设置node1.next=node2;node2.prev=node1。同时将node2设置成尾节点。
    • 添加节点3:步骤如同添加节点2。
      这里写图片描述

acquire()

  1. 在AQS中acquire就是线程在独占模式下获取锁的方式,也是ReentrantLock中加锁lock方法中调用的底层方法。
  2. acquire方法调用tryAcquire方法实现获取锁,即上述子类需要实现的tryAcquire方法。如果tryAcquire方法获取成功,则线程获得执行锁。否则,当前线程进入同步队列阻塞,直到获取锁定或者被中断。
  3. 分析一下acquire源码:
 /** 如果tryAcquire成功(arg=1),则该线程获得执行锁。
 ** 如果tryAcquire失败,addWaiter方法是构造一个Node然后加入到FIFO队列尾部
 ** acquireQueued方法是指从同步队列中的结点都以自旋的形式获取同步状态,获取到同
 ** 步状态的结点,将自身设置为头结点。
 **/
 public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // accquireQueued成功后,重置status为true
            selfInterrupt();
    }

tryAcquire

  1. 独占锁模式下tryAcquire方法是由NonFairSync实现,本身又调用了NonFairSync的方法nonfairTryAcquire。这个方法是功能很简单。主要有2部分构成。
    1.1. 获取锁成功,调用compareAndSetState (0,1)非阻塞的并发方法。成功之后,设置当前 锁的拥有者为当前线程:setExclusiveOwnerThread(currentThread)。
    1.2. 第二部分是,ReentrantLock实现可重入锁的实现。
 final boolean nonfairTryAcquire(int acquires) {
           // 当前线程对象current
            final Thread current = Thread.currentThread();
           // 阻塞线程初始状态都为0
            int c = getState();
            if (c == 0) {
                // 设置获取同步状态,成功设置为1,
                if (compareAndSetState(0, acquires)) {
                    // 设置当前线程拥有锁的权力
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 下面是为ReentrantLock的支持重入锁设计
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

addWaiter

  1. 这个方法是创建节点Node并且将结点加入同步队列尾部。线程在获取锁阻塞时,调用此方法将当前线程对象,等待状态等信息封装成Node对象,插入到同步队列的尾部
  2. 分析addWaiter源码
 private Node addWaiter(Node mode) {
       // 将当前执行线程以独占模式构造成Node对象,mode是独占模式。
       // node作为尾结点加入
        Node node = new Node(Thread.currentThread(), mode);
        //pred!=null,判断尾节点指向引用是否为空,即是否为空队列,第一次构建同步
        //队列时是为空。如果为空,直接执行enq方法。
        Node pred = tail;
        if (pred != null) {
           // 新加入结点node的前驱结点指向原来的尾结点
            node.prev = pred;
            //调用同步方法重新设置尾结点,当前节点即是尾节点tail。
            // tail引用指向node尾节点
            if (compareAndSetTail(pred, node)) {
            // 原尾结点的next指向新的尾结点
                pred.next = node;
                return node;
            }
        }
        // 首次构建队列时,才会执行该方法
        enq(node);
        return node;
    }

enq

该方法以自旋的形式实现。当第一次构建队列时,执行该方法。

/**
** 构造同步队列,初始化head,tail值。
**/
 private Node enq(final Node node) {
        for (;;) {// 以自旋形式运行,直到满足条件才调出结束运行
            Node t = tail;
            // 第一次加入阻塞的结点时,必须初始化head和tail值。
            if (t == null) {
            // 设置头结点,线程安全并发操作。只会有一个线程成功。
            // head和tail指向一个空节点,head和tail此时相同,并且没有持有当前
            // 阻塞线程的引用以及状态。 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            // 自旋一次之后,head和tail节点不为空,将node结点加入到链表中
            // 当前结点的前驱结点为头结点。
                node.prev = t;
            // 设置尾结点,将当前节点node设置为尾节点。
                if (compareAndSetTail(t, node)) {
             // tail指向当前尾结点,即tail就是node节点。
             // 最终head.next=node,node.prev=head。tail=node。
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued

  1. 该方法展示了在并发环境中,所有线程都在同步队列中以自旋的形式等待判断自己当前结点是否是头结点。只有currentNode的前驱结点(prevNode)是头结点(headNode)时,该currentNode才有可能获得同步状态。即currentNode获得同步锁时,需要将自己本身设置为头节点。可以这样理解:头结点的含义就是相当于获得执行的锁
  2. shouldParkAfterFailedAcquire将阻塞的线程状态由0设置为-1和检查curentNode的前驱结点状态,如果是1,则丢弃该节点。
  3. parkAndCheckInterrupt当前阻塞线程调用LockSupport.park方法进行阻塞,等待唤醒(响应中断或者获得锁定)
  4. 该方法的流程如下:这里写图片描述
  5. 方法源码如下,分析:
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)) {
                 // 设置头结点,释放当前线程。其他无用属性设置为Null,进行GC
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                /** 
                ** shoundParkAfterFailedAcquire将同步队列中的节点
                ** 状态由0设置为-1和检查当前节点的前驱结点状态。
                ** parkAndCheckInterrupt将状态为-1的节点进行阻塞,调用
                **LockSupport.park方法完成,等待唤醒。
                **/
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

  1. 该方法主要判断currentNode的前驱结点prevNode的状态。根据prevNode状态进行后续操作。
    1.1 prevNode的状态为-1,则执行parkAndCheckInterrupt方法中断当前线程
    1.2 prevNode状态为1,即大于0.则直接忽略该结点。从同步队列中取消。
    1.3 其他状态(新加入节点是0,其他没有),调用compareAndSetWaitStatus设置prevNode状态为-1
  2. 该方法功能流程如下:
    这里写图片描述
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      //当前结点的前驱结点的状态,无论前驱结点是否是头结点
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 状态为-1,prevNode释放同步状态后,会通         
            return true;       //知其后继结点
        if (ws > 0) {// 值大于0,即为1,已取消状态,则前驱结点直接从队列中删除
            do {     // 当前结点的前驱结点直接指向前驱结点的前驱结点,即跳过
               // currentNode的前驱结点。原前驱结点引用指向新的prevNode
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //currentNode的新前驱结点引用指向当前结点
            pred.next = node;
        } else {
           //尝试设置prevNode的状态为-1,Node.SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt

  1. 同步队列中的节点进行阻塞,等待唤醒。
 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

并发同步方法

在AQS中,多个线程进行并发操作而又不阻塞的操作是通过CAS机制进行保证的。AQS中封装了Unsafe的很多并发操作方法。
1. compareAndSetHead(compareAndSetHead)方法,设置头结点

 /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
  1. compareAndSetTail(Node expect, Node update),设置尾结点
 /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
  1. compareAndSetWaitStatus(Node node,int expect, int update)设置等待状态
/**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }
  1. compareAndSetNext(Node node,Node expert,Node update)设置当前节点后继节点
 /**
     * CAS next field of a node.
     */
    private static final boolean compareAndSetNext(Node node,
                                                   Node expect,
                                                   Node update) {
        return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
    }

未完待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值