谈下: AQS的原理

这是一道面试题:简述AQS原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

 

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

看个AQS(AbstractQueuedSynchronizer)原理图:

AQS,它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

getState()
setState()
compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。


不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
 

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
 

从源码的角度来分析(以ReentrantLock为例子

所有的jdk实现的锁ReentrantLock、CountDownLatch、Semaphore通过内部匿名类继承了AbstractQueuedSynchronizer,然后重写AQS提供的部分方法。

在讲解reentrantlock之前先来看一下AQS的一些细节东西,因为reentrantlock类里面有三个静态内部类,这些静态内部类里面有些方法会用到AQS里面的方法,具体有哪些方法后面会提到,这里只是提下有个印象,三个静态内部类分别为:

//实现了AQS中的tryRelease(int releases),isHeldExclusively()方法,至于是抽象的是因为没有实现lock方法和
tryAcquire(int arg)的方法,因为这些方法是有其字类来实现的。
abstract static class Sync extends AbstractQueuedSynchronizer{} 
static final class NonfairSync extends Sync{}
static final class FairSync extends Sync {}

      下面部分图和文字参考了一些大神的。大神连接走你

1、原理及架构
AQS内部维护一个同步队列,所有的同步队列中的线程通过自旋方式不断获取同步状态。当一个线程无法获取同步状态时,通过创建一个Node节点并将此节点加入队列的尾部,此队列严格按照FIFO方式出入队列,只有当前线程是头结点并且成功获取到同步状态,此时的线程就可出队列;

state在AQS中被定义为一个volatile类型的的变量通过提供两种方法来让子类设置同步状态分别是compareAndSetState和setState,一种是通过原子CAS的方式,一种是普通方式,大致的架构如下:

首先看看Node(为AQS的一个静态内部类)节点的几个重要属性:

  • SHARED、EXCLUSIVE表示节点的模式,在创建节点是指定一种模式,一般情况比如对于文件系统的读操作,可以设置为共享模式,对于文件的写要设置为独占式保证线程安全的执行
  • waitStatus:节点的状态为三种:

1、CANCELLED表示线程处于超时或者中断状态,值为1

2、SIGNAL表示后继节点被唤醒,值为-1

3、CONDITION表示线程处于阻塞队列中,无法在同步队列中使用,值为-2,直到调用signal方法后将其转移到同步队列中

4、PROPAGATE表示下一个共享模式下获取同步状态会被持续传播下去,值为-3

 

一般情况AQS获取同步状态的方式分为共享模式和独占模式,下面先分析一下独占模式下AQS是如何获取一个同步状态的:其实它使用了一个模板设计模式。

tryAcquire(arg)方法有上面提到的字类方法去实现

    // 独占模式下获取同步状态
    public final void acquire(int arg) {
        //尝试获取同步状态,如果获取失败,创建节点并且将节点加入到同步队列的尾部,并进行自旋状态,同时设置为阻塞状态
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

由于tryAcquire方法需要子类去实现,先分析一下这里的addWaiter:

 //入参Mode为节点的模式,这里将获取不到状态的线程加入到同步队列中
    private Node addWaiter(Node mode) {
        //首先创建一个线程节点
        Node node = new Node(Thread.currentThread(), mode);
        //获取尾节点赋值当做新增节点的前驱
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //进行cas原子操作将新增节点加入到尾部,同时将前驱节点的后继节点设置为当前节点并返回
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果不存在同步队列或者尾部不存在
        enq(node);
        return node;
    }

想一下:为什么要用CAS操作把当前线程node放入链表尾部节点?

答:因为,是多个线程去获取锁,如都没获取成功,则是不是都要把各自的node放入链表尾部,这样就有了并发了,因此加了CAS操作。

上述操作的图解如下图

addWaiter(node)方法中到数第二行有个enq方法,我们再来看一下:enq(node)方法

enq方法通过cas方式先去找尾节点,若为null,则初始化同步队列 这时,tail=head,否则按照正常的将新节点加入到尾部如上图所示。

 //通过cas死循环
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 

接着分析 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法:当新的线程未获取到同步状态后即上面代码中tryAcquire(arg)获取资源失败,则加入到同步队列中,此时通过此方法进行自旋,如下图所示:

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前线程节点的前驱节点,如果是head节点则再次尝试获取同步状态,获取成功,将自身设置为头节点,前驱节点出队列
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //获取同步状态失败则设置该节点的状态,并且阻塞该节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

进行自旋时一旦发现前驱节点为head节点并且尝试获取同步状态,获取成功则设置当前节点为头节点,如下图所示:

shouldParkAfterFailedAcquire方法为当节点未获取同步状态时设置该节点的waitingStatus的状态,如果是被阻塞了直接返回true。

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //如果前驱节点已经被唤醒,则它是安全的被阻塞了,直接返回true
            return true;
        if (ws > 0) {
            //如果当前节点的前驱节点状态为cancelled,则进入循环直到找到不为cancelled的节点,把此节点设置为当前节点的前驱节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //将前驱节点设置为signal
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 

独占模式下同步状态的释放流程release方法:

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //获取头部节点不为Null,则唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

同理tryRelease(arg)方法由其字类方法去实现,这里以ReentrantLock为例子,它的静态内部类Sys类实现了这个方法。

这里重点看看unparkSuccessor方法,当唤醒后继节点后,由于进入同步队列的线程都处于不停地自旋状态,一旦符合前驱是head并且获取到同步节点则进行出队列

 private void unparkSuccessor(Node node) {
        //获取head节点的状态如果状态不是初始状态或cancelled,则设置为初始态
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
​
        //如果后继节点是取消状态或者为Null,则循环从尾部节点开始往前找
        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);
    }

 

 

 

综上所述,AQS的一些原理讲解完了,下面结合例子ReentrantLock类默认非公平锁,看看如何集合AQS来使用的。

ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。上面已经提到,为了加深印象,这里再次提到其代码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {}

static final class NonfairSync extends Sync {}

static final class FairSync extends Sync {}

(1)抽象类Sync实现了AQS的部分方法如实现了AQS中的tryRelease(int releases),isHeldExclusively()方法,至于是抽象的是因为没有实现lock方法和tryAcquire(int arg)的方法,因为这些方法是有其字类来实现的。

(2)NonfairSync实现了Sync,主要用于非公平锁的获取。

(3)FairSync实现了Sync,主要用于公平锁的获取。

在这里我们先不急着看每个类具体的代码,等下面学习具体的功能点的时候再把所有方法串起来。

ReentrantLock中主要属性:private final Sync sync;

主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。

ReentrantLock中构造函数:

// 默认构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
// 自己可选择使用公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

 着重分析lock()方法,看他如何一步步和AQS结合的,默认是非公平锁,分析也是从非公平锁为例子。

接着跟进sys的lock函数,可以看到是个抽象函数,结合上面提到的是由其字类NonfairSync 来实现。

因此,我们跟进NonfairSync 的lock函数

final void lock() {
// 直接尝试CAS更新状态变量
if (compareAndSetState(0, 1))
// 如果更新成功,说明获取到锁,把当前线程设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

从上述方法中看出,如果CAS更新state成功,则把当前线程设置为独占式线程,如果失败调用acquire(1)方法,把当前线程的信息分装成node节点加入到链表中(这里会和上面的AQS的一些代码结合起来,使用模板设计模式,我们看他是如何结合起来的)。

接着跟踪到acquire(1)方法,此方法在AQS里面被定义,tryAcquire(arg)在AQS里面是抽象方法,进去acquire方法后执行tryAcquire方法会进入NonfairSync(ReentranceLock的静态内部类)的tryAcquire方法里面,这也是使用模板设计模式来实现此效果的。

  public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法前面已经分析过了,我们跟进tryAcquire方法

protected final boolean tryAcquire(int acquires) {
// 调用父类Sys的方法
return nonfairTryAcquire(acquires);
}

接着跟进其父类Sys(ReentranceLock的静态内部类)的nonfairTryAcquire方法。

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
              // 如果状态变量的值为0,再次尝试CAS更新状态变量的值
              // 相对于公平锁模式少了!hasQueuedPredecessors()条件
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
             可重用的条件
            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;
        }

相对于公平锁,非公平锁加锁的过程主要有两点不同:

(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;

(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;

总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。

 

分析完加锁了,接着分析unlock方法,看代码是不是很熟悉了,上述上面分析过了但是没有分析

tryRelease方法。
public void unlock() {
sync.release(1);
}

 

 

接着分析tryRelease()方法,这方法在哪里实现的就不说了,看到这里你肯定知道在哪里实现的。

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
          // 如果当前线程不是占有着锁的线程,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
           // 如果状态变量的值为0了,说明完全释放了锁
           // 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
           // 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
            if (c == 0) {
                free = true;
               // 清空占有线程
                setExclusiveOwnerThread(null);
            }
           // 设置状态变量的值
            setState(c);
            return free;
        }

有一个细节要注意,上面方法设置状态变量的值即setState(c)为什么不用CAS操作?

答:因为是同一个线程,没有线程冲突。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值