首页 > 编程知识 正文

0aqs(aqs详解)

时间:2023-05-05 12:22:41 阅读:78503 作者:3207

介绍

AQS的统称(AbstractQueuedSynchronizer ),该类位于java.util.concurrent.locks包下。

AQS是构建锁定和同步器的框架,使用AQS时,我们提到的ReentrantLock、Semaphore、其他ReentrantReadWriteLock、SynchronousQueue 可以轻松高效地构建广泛的同步器,当然,我们自己也可以利用AQS非常容易地构建符合我们自身需求的同步器。

AQS 原理

面试中被问到同时知识时,经常会被问到“请说一下你对AQS原理有什么理解”。 举个例子作为参考吧。 面试不是暗语。 请大家一定要把自己的想法放进去。 即使不能加入自己的想法,也请不要死记硬背,而要说得通俗易懂。

下面的大部分内容实际上都显示在AQS类注释中,但是用英语看起来有点困难,如果你感兴趣的话可以看看源代码。

AQS 原理概览

AQS的核心思想是,在请求的共享资源空闲时,将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态。 如果请求的共享资源被占用,则线程必须阻止并等待,并且必须有一种机制在唤醒时分配锁定。 此机制AQS通过CLH队列锁定实现,它将临时无法获取锁定的线程添加到队列中。

CLH(Craig、Landin、and Hagersten )队列是虚拟双向队列(虚拟双向队列不存在队列实例,仅存在节点之间的关联关系)。 AQS将请求共享资源的每个线程封装在CLH锁定队列的节点(Node )中,以实现锁定分配。 AQS(abstractqueuedsynchronizer )原理图如下。

AQS使用int成员变量来表示同步状态,并完成用于使用内置FIFO队列检索资源线程的队列。 AQS使用CAS原子操作此同步状态并更改其值。

隐私保护状态; //共享变量,并使用volatile修饰确保线程可见性

//返回同步状态的当前值

protectedfinalintgetState (

返回状态;

}

//设置同步状态的值

protectedfinalvoidsetstate (内联网)。

state=newState;

}

//将原子(CAS操作)同步状态的值设定为规定的值update当前的同步状态的值与expect (期待值)相等的情况下

protectedfinalbooleancompareandsetstate (int expect,intupdate ) )。

return unsafe.compareandswapint (this,stateOffset,expect,update );

}

AQS 对资源的共享方式

AQS定义了两种资源共享方法

1 ) Exclusive )垄断

只有一个线程可以像ReentrantLock一样运行。 分为公平锁定和非公平锁定。 ReentrantLock支持两种锁定。 以下是ReentrantLock的定义。

公平锁:按照线程排队的顺序,先到者首先获得锁。 如果线程获得锁定,它将首先尝试通过两次CAS操作来获取锁定,如果未成功,当前线程将重新加入队列并等待唤醒。

2 )份额

可以同时运行多个线程,如Semaphore/CountDownLatch。 稍后将介绍Semaphore、CountDownLatCh、CyclicBarrier和ReadWriteLock。

ReentrantReadWriteLock (读/写锁定)可以将ReentrantReadWriteLock视为一种组合,因为多个线程可以同时读取单个资源。

的自定义同步器不同,共享资源的竞争方法也不同。 自定义同步器在实现时只需要实现共享资源state的获取和释放方法,对于特定线程等待队列维护,AQS已经在上层实现。

AQS 底层使用了

模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。

模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票buyTicket()->安检securityCheck()->乘坐某某工具回家ride()->到达目的地arrive()。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了ride()方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 ride()方法。

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

默认情况下,每个方法都抛出 UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

以 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()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

核心源码

private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode);//构造结点 //指向尾结点tail Node pred = tail; //如果尾结点不为空,CAS快速尝试在尾部添加,若CAS设置成功,返回;否则,eng。 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node;     } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { //如果队列为空,创建结点,同时被head和tail引用 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) {//cas设置尾结点,不成功就一直重试 t.next = node; return t; } } } } 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)) {//如果前驱结点是头结点,才tryAcquire,其他结点是没有机会tryAcquire的。 setHead(node);//获取同步状态成功,将当前结点设置为头结点。 p.next = null; // 方便GC failed = false; return interrupted; } // 如果没有获取到同步状态,通过shouldParkAfterFailedAcquire判断是否应该阻塞,parkAndCheckInterrupt用来阻塞线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。