.net wap网站模板,揭阳网站开发,浙江均泰建设有限公司网站,网络规划设计师科目分类2019独角兽企业重金招聘Python工程师标准 AQS的原理浅析 本文是《Java特种兵》的样章#xff0c;本书即将由工业出版社出版 AQS的全称为#xff08;AbstractQueuedSynchronizer#xff09;#xff0c;这个类也是在java.util.concurrent.locks下面。这个类似乎… 2019独角兽企业重金招聘Python工程师标准 AQS的原理浅析 本文是《Java特种兵》的样章本书即将由工业出版社出版 AQS的全称为AbstractQueuedSynchronizer这个类也是在java.util.concurrent.locks下面。这个类似乎很不容易看懂因为它仅仅是提供了一系列公共的方法让子类来调用。那么要理解意思就得从子类下手反过来看才容易看懂。如下图所示 图 5-15 AQS的子类实现 这么多类我们看那一个刚刚提到过锁Lock我们就从锁开始吧。这里就先以ReentrantLock排它锁为例开始展开讲解如何利用AQS的然后再简单介绍读写锁的要点读写锁本身的实现十分复杂要完全说清楚需要大量的篇幅来说明。 首先来看看ReentrantLock的构造方法它的构造方法有两个如下图所示 图 5-16 排它锁的构造方法 很显然对象中有一个属性叫sync有两种不同的实现类默认是“NonfairSync”来实现而另一个“FairSync”它们都是排它锁的内部类不论用那一个都能实现排它锁只是内部可能有点原理上的区别。先以“NonfairSync”类为例它的lock()方法是如何实现的呢 图 5-17 排它锁的lock方法 lock()方法先通过CAS尝试将状态从0修改为1。若直接修改成功前提条件自然是锁的状态为0则直接将线程的OWNER修改为当前线程这是一种理想情况如果并发粒度设置适当也是一种乐观情况。 若上一个动作未成功则会间接调用了acquire(1)来继续操作这个acquire(int)方法就是在AbstractQueuedSynchronizer当中了。这个方法表面上看起来简单但真实情况比较难以看懂因为第一次看这段代码可能不知道它要做什么不急一步一步来分解。 首先看tryAcquire(arg)这里的调用当然传入的参数是1在默认的“NonfairSync”实现类中会这样来实现 妈呀这代码好费劲胖哥第一回看也是觉得这样细心看看也不是想象当中那么难 ○ 首先获取这个锁的状态如果状态为0则尝试设置状态为传入的参数这里就是1若设置成功就代表自己获取到了锁返回true了。状态为0设置1的动作在外部就有做过一次内部再一次做只是提升概率而且这样的操作相对锁来讲不占开销。 ○ 如果状态不是0则判定当前线程是否为排它锁的Owner如果是Owner则尝试将状态增加acquires也就是增加1如果这个状态值越界则会抛出异常提示若没有越界将状态设置进去后返回true实现了类似于偏向的功能可重入但是无需进一步征用。 ○ 如果状态不是0且自身不是owner则返回false。 回到图 5-17中对tryAcquire()的调用判定中是通过if(!tryAcquire())作为第1个条件的如果返回true则判定就不会成立了自然后面的acquireQueued动作就不会再执行了如果发生这样的情况是最理想的。 无论多么乐观征用是必然存在的如果征用存在则owner自然不会是自己tryAcquire()方法会返回false接着就会再调用方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)做相关的操作。 这个方法的调用的代码更不好懂需要从里往外看这里的Node.EXCLUSIVE是节点的类型看名称应该清楚是排它类型的意思。接着调用addWaiter()来增加一个排它锁类型的节点这个addWaiter()的代码是这样写的 图 5-19 addWaiter的代码 这里创建了一个Node的对象将当前线程和传入的Node.EXCLUSIVE传入也就是说Node节点理论上包含了这两项信息。代码中的tail是AQS的一个属性刚开始的时候肯定是为null也就是不会进入第一层if判定的区域而直接会进入enq(node)的代码那么直接来看看enq(node)的代码。 看到了tail就应该猜到了AQS是链表吧没错而且它还应该有一个head引用来指向链表的头节点AQS在初始化的时候head、tail都是null在运行时来回移动。此时我们最少至少知道AQS是一个基于状态state的链表管理方式。 图 5-20 enq(Node)的源码 这段代码就是链表的操作某些同学可能很牛一下就看懂了某些同学一扫而过觉得知道大概就可以了某些同学可能会莫不着头脑。胖哥为了给第三类同学来“开开荤”简单讲解下这个代码。 首先这个是一个死循环而且本身没有锁因此可以有多个线程进来假如某个线程进入方法此时head、tail都是null自然会进入if(t null)所在的代码区域这部分代码会创建一个Node出来名字叫h这个Node没有像开始那样给予类型和线程很明显是一个空的Node对象而传入的Node对象首先被它的next引用所指向此时传入的node和某一个线程创建的h对象如下图所示。 图 5-21 临时的h对象创建后的与传入的Node指向关系 刚才我们很理想的认为只有一个线程会出现这种情况如果有多个线程并发进入这个if判定区域可能就会同时存在多个这样的数据结构在各自形成数据结构后多个线程都会去做compareAndSetHead(h)的动作也就是尝试将这个临时h节点设置为head显然并发时只有一个线程会成功因此成功的那个线程会执行tail node的操作整个AQS的链表就成为 图 5-22 AQS被第一个请求成功的线程初始化后 有一个线程会成功修改head和tail的值其它的线程会继续循环再次循环就不会进入if (t null)的逻辑了而会进入else语句的逻辑中。 在else语句所在的逻辑中第一步是node.prev t这个t就是tail的临时值也就是首先让尝试写入的node节点的prev指针指向原来的结束节点然后尝试通过CAS替换掉AQS中的tail的内容为当前线程的Node无论有多少个线程并发到这里依然只会有一个能成功成功者执行t.next node也就是让原先的tail节点的next引用指向现在的node现在的node已经成为了最新的结束节点不成功者则会继续循环。 简单使用图解的方式来说明3个步骤如下所示如下图所示 图 5-23 插入一个节点步骤前后动作 插入多个节点的时候就以此类推了哦总之节点都是在链表尾部写入的而且是线程安全的。 知道了AQS大致的写入是一种双向链表的插入操作但插入链表节点对锁有何用途呢我们还得退回到前面图 5-19的代码中addWaiter方法最终返回了要写入的node节点 再回退到图5-17中所在的代码中需要将这个返回的node节点作为acquireQueued方法入口参数并传入另一个参数依然是1看看它里面到底做了些什么请看下图 图 5-24 acquireQueued的方法内容 这里也是一个死循环除非进入if(p head tryAcquire(arg))这个判定条件而p为node.predcessor()得到这个方法返回node节点的前一个节点也就是说只有当前一个节点是head的时候进一步尝试通过tryAcquire(arg)来征用才有机会成功。tryAcquire(arg)这个方法我们前面介绍过成立的条件为锁的状态为0且通过CAS尝试设置状态成功或线程的持有者本身是当前线程才会返回true我们现在来详细拆分这部分代码。 ○ 如果这个条件成功后发生的几个动作包含 1 首先调用setHead(Node)的操作这个操作内部会将传入的node节点作为AQS的head所指向的节点。线程属性设置为空因为现在已经获取到锁不再需要记录下这个节点所对应的线程了再将这个节点的perv引用赋值为null。 2 进一步将的前一个节点的next引用赋值为null。 在进行了这样的修改后队列的结构就变成了以下这种情况了通过这样的方式就可以让执行完的节点释放掉内存区域而不是无限制增长队列也就真正形成FIFO了 图 5-25 CAS成功获取锁后队列的变化 ○ 如果这个判定条件失败 会首先判定“shouldParkAfterFailedAcquire(p , node)”这个方法内部会判定前一个节点的状态是否为“Node.SIGNAL”若是则返回true若不是都会返回false不过会再做一些操作判定节点的状态是否大于0若大于0则认为被“CANCELLED”掉了我们没有说明几个状态的值不过大于0的只可能被CANCELLED的状态因此会从前一个节点开始逐步循环找到一个没有被“CANCELLED”节点然后与这个节点的next、prev的引用相互指向如果前一个节点的状态不是大于0的则通过CAS尝试将状态修改为“Node.SIGNAL”自然的如果下一轮循环的时候会返回值应该会返回true。 如果这个方法返回了true则会执行“parkAndCheckInterrupt()”方法它是通过LockSupport.park(this)将当前线程挂起到WATING状态它需要等待一个中断、unpark方法来唤醒它通过这样一种FIFO的机制的等待来实现了Lock的操作。 相应的可以自己看看FairSync实现类的lock方法其实区别不大有些细节上的区别可能会决定某些特定场景的需求你也可以自己按照这样的思路去实现一个自定义的锁。 接下来简单看看unlock()解除锁的方式如果获取到了锁不释放那自然就成了死锁所以必须要释放来看看它内部是如何释放的。同样从排它锁ReentrantLock中的unlock()方法开始请先看下面的代码截图 图 5-26 unlock方法间接调用AQS的release(1)来完成 通过tryRelease(int)方法进行了某种判定若它成立则会将head传入到unparkSuccessor(Node)方法中并返回true否则返回false。首先来看看tryRelease(int)方法如下图所示 图 5-27 tryRelease(1)方法 这个动作可以认为就是一个设置锁状态的操作而且是将状态减掉传入的参数值参数是1如果结果状态为0就将排它锁的Owner设置为null以使得其它的线程有机会进行执行。 在排它锁中加锁的时候状态会增加1当然可以自己修改这个值在解锁的时候减掉1同一个锁在可以重入后可能会被叠加为2、3、4这些值只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空而且也只有这种情况下才会返回true。 这一点大家写代码要注意了哦如果是在循环体中lock()或故意使用两次以上的lock(),而最终只有一次unlock()最终可能无法释放锁。在本书的src/chapter05/locks/目录下有相应的代码大家可以自行测试的哦。 在方法unparkSuccessor(Node)中就意味着真正要释放锁了它传入的是head节点head节点是已经执行完的节点在后面阐述这个方法的body的时候都叫head节点内部首先会发生的动作是获取head节点的next节点如果获取到的节点不为空则直接通过“LockSupport.unpark()”方法来释放对应的被挂起的线程这样一来将会有一个节点唤醒后继续进入图 5-24中的循环进一步尝试tryAcquire()方法来获取锁但是也未必能完全获取到哦因为此时也可能有一些外部的请求正好与之征用而且还奇迹般的成功了那这个线程的运气就有点悲剧了不过通常乐观认为不会每一次都那么悲剧。 再看看共享锁从前面的排它锁可以看得出来是用一个状态来标志锁的而共享锁也不例外但是Java不希望去定义两个状态所以它与排它锁的第一个区别就是在锁的状态上它用int来标志锁的状态int有4个字节它用高16位标志读锁共享锁低16位标志写锁排它锁高16位每次增加1相当于增加65536通过1 16得到自然的在这种读写锁中读锁和写锁的个数都不能超过65535个条件是每次增加1的如果递增是跳跃的将会更少。在计算读锁数量的时候将状态左移16位而计算排它锁会与65535“按位求与”操作如下图所示。 图 5-28 读写锁中的数量计算及限制 写锁的功能与“ReentrantLock”基本一致区域在于它会在tryAcquire操作的时候判定状态的时候会更加复杂一些因此有些时候它的性能未必好。 读锁也会写入队列Node的类型被改为“Node.SHARED”这种类型lock()时候调用的是AQS的acquireShared(int)方法进一步调用tryAcquireShared()操作里面只需要检测是否有排它锁如果没有则可以尝试通过CAS修改锁的状态如果没有修改成功则会自旋这个动作可能会有很多线程在这自旋开销CPU。如果这个自旋的过程中检测到排它锁竞争成功那么tryAcquireShared()会返回-1从而会走如排它锁的Node类似的流程可能也会被park住等待排它锁相应的线程最终调用unpark()动作来唤醒。 这就是Java提供的这种读写锁不过这并不是共享锁的诠释在共享锁里面也有多种机制 或许这种读写锁只是其中一种而已。在这种锁下面读和写的操作本身是互斥的但是读可以多个一起发生。这样的锁理论上是非常适合应用在“读多写少”的环境下当然我们所讲的读多写少是读的比例远远大于写而不是多一点点理论上讲这样锁征用的粒度会大大降低同时系统的瓶颈会减少效率得到总体提升。 在本节中我们除了学习到AQS的内在还应看到Java通过一个AQS队列解决了许多问题这个是Java层面的队列模型其实我们也可以利用许多队列模型来解决自己的问题甚至于可以改写模型模型来满足自己的需求在本章的5.6.1节中将会详细介绍。 关于Lock及AQS的一些补充 1、 Lock的操作不仅仅局限于lock()/unlock()因为这样线程可能进入WAITING状态这个时候如果没有unpark()就没法唤醒它可能会一直“睡”下去可以尝试用tryLock()、tryLock(long , TimeUnit)来做一些尝试加锁或超时来满足某些特定场景的需要。例如有些时候发现尝试加锁无法加上先释放已经成功对其它对象添加的锁过一小会再来尝试这样在某些场合下可以避免“死锁”哦。 2、 lockInterruptibly() 它允许抛出InterruptException异常也就是当外部发起了中断操作程序内部有可能会抛出这种异常但是并不是绝对会抛出异常的大家仔细看看代码便清楚了。 3、 newCondition()操作是返回一个Condition的对象Condition只是一个接口它要求实现await()、awaitUninterruptibly()、awaitNanos(long)、await(long , TimeUnit)、awaitUntil(Date)、signal()、signalAll()方法AbstractQueuedSynchronizer中有一个内部类叫做ConditionObject实现了这个接口它也是一个类似于队列的实现具体可以参考源码。大多数情况下可以直接使用当然觉得自己比较牛逼的话也可以参考源码自己来实现。 4、 在AQS的Node中有每个Node自己的状态waitStatus我们这里归纳一下分别包含: SIGNAL 从前面的代码状态转换可以看得出是前面有线程在运行需要前面线程结束后调用unpark()方法才能激活自己值为-1 CANCELLED 当AQS发起取消或fullyRelease()时会是这个状态。值为1也是几个状态中唯一一个大于0的状态所以前面判定状态大于0就基本等价于是CANCELLED的意思。 CONDITION 线程基于Condition对象发生了等待进入了相应的队列自然也需要Condition对象来激活值为-2。 PROPAGATE 读写锁中当读锁最开始没有获取到操作权限得到后会发起一个doReleaseShared()动作内部也是一个循环当判定后续的节点状态为0时尝试通过CAS自旋方式将状态修改为这个状态表示节点可以运行。 状态0 初始化状态也代表正在尝试去获取临界资源的线程所对应的Node的状态。 转载于:https://my.oschina.net/u/1185936/blog/857268