热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

并发编程浅谈AQS源码

目录一、ReentrantLock二、AQS1.和ReentrantLock的关系2.AQS队列同步器源码分析同步队列:独占式同步队列状态获取和释放ÿ



目录

一、ReentrantLock 

二、AQS

1.和ReentrantLock的关系

2.AQS队列同步器源码分析

同步队列:

 独占式同步队列状态获取和释放: 

释放锁并且唤醒一下一个处于part挂起状态的线程:




一、ReentrantLock 

        在Lock接口出现之前,Java程序主要是靠Synchronized关键字来实现锁的功能,但是在Java 5之后在并发包中增加了Lock接口来实现锁的功能,其中ReentrantLock就是Lock接口的一个比较常用的实现类

        相比Synchronized(Synchronized的学习可以看本系列博客“”)来说,ReentrantLock有跟多的优势:


  1. 释放锁方面: synchronized 只有在异常发生或者同步块执行完之后才会释放锁 但是ReentrantLock可以通过 unlock()来灵活的释放锁资源
  2. 灵活性: ReentrantLock更加灵活
  3. 可以尝试非阻塞的获取锁,利用tryLock() 如果获取成功就返回true,否则返回false,不会一直堵塞
  4. 中断的获取锁
  5. 超时获取锁

二、AQS


1.和ReentrantLock的关系

在ReentrantLock聚合了同步器,利用同步器来真实的实现了锁的语义,也可以说ReentrantLock是面向使用者的,同步器是锁的真实实现

结构图: 

image.png


  • Lock是锁的统一接口,ReentrantLock是Lock的一个具体实现类
  • Sync是ReentrantLock的静态内部类,该类继承了  AbstractQueuedSynchronizer
  • NofaitSync和FaitSync是Sync的两个子类

整理来看AQS使用了模板方法,该图是本人对于AQS使用整理的模板方法关系图,关系读者于评论区一起讨论

 

 


2.AQS队列同步器源码分析


同步队列:

        同步队列是一个FIFO(先进先出)的双向队列,当线程获取锁资源失败的时候会变为NODE节点,并且加入到同步队列中,同时会堵塞当前线程,当有线程释放锁资源的时候,会唤醒同步队列的首节点

1. 线程获取锁失败,线程加入队列:

        当有新的线程获取锁资源失败的时候会加入到队列的尾,但是由于是多线程执行的,可能会有多个线程同时需要加入到同步队列的尾,所以我们这个时候利用CAS来保证线程安全,他需要传递当前线程认为的尾节点和当前节点,否则会由于多个线程共同插入,队列混乱。

CompareAndSetTail(Node expect,Node update)

2. 线程获取到锁,并将释放锁的节点移除同步队列:

        首节点是获取锁成功的节点,首节点的线程在释放锁时,会唤醒后续节点,而后继节点在成功获取到锁后,会把自己设置成首节点,设置首节点是由获取锁成功的线程来完成的,由于只有一个线程能成功获取到锁,所以设置首节点不需要CAS


 独占式同步队列状态获取和释放: 

//lock方法先通过CAS尝试将同步状态(AQS的state属性)从0修改为1。
//若直接修改成功了,则将占用锁的线程设置为当前线程
final void lock() {if (compareAndSetState(0, 1))//用来保存当前占用同步状态的线程。setExclusiveOwnerThread(Thread.currentThread());else//独占式获取同步状态,如果获取成功就直接返回,否则加入同步队列acquire(1);}

解释: 同步队列中维护了一个 state 变量,当state 变量为0的时候表示当前锁没有被获取,当有一个线程想要获取到锁的时候,首先会通过CAS去判断当前state是否为0 ,如果为0就抢占锁资源,设置当前线程为同步状态的线程,因为ReentrantLock是支持可重入的,所以当某一个线程判断当前状态不为0,他是有机会继续获取同步状态的(同一个线程可以重入)

看acquire方法:

/**
* tryAcquire方法尝试获取锁,如果成功就返回,
* 如果不成功,则把当前线程和等待状态信息构适成一个Node节点,
* 并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功
*
*/public final void acquire(int arg) {//这里调用的是被子类重写的方法,如果没有获取成功if (!tryAcquire(arg) &&//将线程转变为NODE节点,并且加入到同步队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

tryAcquire()方法:

tryAcquire方法是AQS提供的一个可重写的方法,被ReentrantLock的重写成了公平和非公平锁,我们默认走非公平锁.

final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current &#61; Thread.currentThread();//获取状态int c &#61; getState();//如果状态为0表示没有被加锁if (c &#61;&#61; 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//如果发现当前加锁的是当前线程&#xff0c;那么就可以重入else if (current &#61;&#61; getExclusiveOwnerThread()) {//将重入次数&#43;1int nextc &#61; c &#43; acquires;if (nextc <0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

addWaiter(Node.EXCLUSIVE), arg)方法&#xff1a;

private Node addWaiter(Node mode) {Node node &#61; new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred &#61; tail;//如果当前同步队列不为空if (pred !&#61; null) {node.prev &#61; pred;//从尾部加入节点if (compareAndSetTail(pred, node)) {pred.next &#61; node;return node;}}//构建同步队列空的首节点enq(node);return node;}

        首先创建一个Node对象&#xff0c;Node中包含了当前线程和Node模式(这时是排他模式)。tail是AQS的中表示同步队列队尾的属性&#xff0c;刚开始为null&#xff0c;所以进行enq(node)方法&#xff0c;从字面可以看出这是一个入队操作&#xff0c;来看下具体入队细节

private Node enq(final Node node) {for (;;) {Node t &#61; tail;if (t &#61;&#61; null) { // Must initialize//比较并且设置一个空的首节点if (compareAndSetHead(new Node()))tail &#61; head;} else {node.prev &#61; t;if (compareAndSetTail(t, node)) {t.next &#61; node;return t;}}}}

 解析&#xff1a;

        方法体是一个死循环&#xff0c;本身没有锁&#xff0c;可以多个线程并发访问&#xff0c;假如某个线程进入方法&#xff0c;此时head, tail都为null, 进入if(t&#61;&#61;null)区域&#xff0c;从方法名可以看出这里是用CAS的方式创建一个空的Node作为头结点&#xff0c;因为此时队列中只一个头结点&#xff0c;所以tail也指向它&#xff0c;第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时&#xff0c;只有一个线程能够执行成功&#xff0c;防止创建多个同步队列。

进行第二次循环时(或者是其他线程enq时)&#xff0c;tail不为null&#xff0c;进入else区域。将当前线程的Node结点(简称CNode)的prev指向tail&#xff0c;然后使用CAS将tail指向CNode。&#xff08;真实的入队情况&#xff09;

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法解析&#xff1a;

final boolean acquireQueued(final Node node, int arg) {boolean failed &#61; true;try {boolean interrupted &#61; false;for (;;) {//获得当前节点的前驱节点final Node p &#61; node.predecessor();//当前节点的前一个节点是首节点 并且 当前节点能够获取到同步状态if (p &#61;&#61; head && tryAcquire(arg)) {//设置当前节点为首节点setHead(node);p.next &#61; null; // help GCfailed &#61; false;return interrupted;}//否则就判断是否应该挂起if (shouldParkAfterFailedAcquire(p, node) &&//如果应该挂起则执行 挂起parkAndCheckInterrupt())interrupted &#61; true;}} finally {if (failed)cancelAcquire(node);}}

可以看到&#xff0c;acquireQueued方法也是一个死循环&#xff0c;直到进入 if (p &#61;&#61; head && tryAcquire(arg))条件方法块。

如果当前节点的前一个节点不是头节点&#xff0c;就无需循环抢锁。

如果抢锁成功&#xff1a;

1) 将CNode设置为头节点。

2) 将CNode的前置节点设置的next设置为null。

上面操作即完成了FIFO的出队操作。

从上面的分析可以看出&#xff0c;只有队列的第二个节点可以有机会争用锁&#xff0c;如果成功获取锁&#xff0c;则此节点晋升为头节点。对于第三个及以后的节点&#xff0c;if (p &#61;&#61; head)条件不成立&#xff0c;首先进行shouldParkAfterFailedAcquire(p, node)操作&#xff08;争用锁失败的第二个节点也如此&#xff09;&#xff0c; 来看下源码&#xff1a;

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws &#61; pred.waitStatus;//如果节点的状态为-1 那就说明状态已经是可以被唤醒的状态&#xff0c;就直接返回true即可if (ws &#61;&#61; Node.SIGNAL)return true;if (ws > 0) {do {node.prev &#61; pred &#61; pred.prev;} while (pred.waitStatus > 0);pred.next &#61; node;} else {//把前置结点变为-1&#xff0c;当前置结点为-1的时候我当前节点就挂起compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL&#xff0c;如果是&#xff0c;是说明此节点已经将状态设置如果锁释放则应当通知它&#xff0c;所以它可以安全的阻塞了&#xff0c;返回true。

如果前节点的状态大于0&#xff0c;即为CANCELLED状态时&#xff0c;则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点&#xff0c;返回false。在下次循环执行shouldParkAfterFailedAcquire时&#xff0c;返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。

前节点状态小于0的情况是对应ReentrantLock的Condition条件等待的&#xff0c;这里不进行展开。

private final boolean parkAndCheckInterrupt() {//线程挂起LockSupport.park(this);//返回一个中断标志是否被中断过&#xff0c;并且复位&#xff0c;为了响应中断return Thread.interrupted();}

如果shouldParkAfterFailedAcquire返回了true&#xff0c;则会执行&#xff1a;“parkAndCheckInterrupt()”方法&#xff0c;它是通过LockSupport.park(this)将当前线程挂起到WATING状态&#xff0c;它需要等待一个中断、unpark方法来唤醒它&#xff0c;通过这样一种FIFO的机制的等待&#xff0c;来实现了Lock的操作。

获取锁的时序图&#xff1a;

image.png

 


释放锁并且唤醒一下一个处于part挂起状态的线程&#xff1a;

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

 unlock调用AQS的release()来完成, AQS的如果tryRelease方法由具体子类实现。tryRelease返回true,则会将head传入到unparkSuccessor(Node)方法中并返回true&#xff0c;否则返回false。

public final boolean release(int arg) {//如果成功if (tryRelease(arg)) {Node h &#61; head;if (h !&#61; null && h.waitStatus !&#61; 0)//唤醒自己的后继节点unparkSuccessor(h);return true;}return false;}

protected final boolean tryRelease(int releases) {//状态值减1int c &#61; getState() - releases;//如果当前线程不等于独占线程if (Thread.currentThread() !&#61; getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free &#61; false;if (c &#61;&#61; 0) {free &#61; true;//设置当前独占线程为空setExclusiveOwnerThread(null);}setState(c);return free;}

private void unparkSuccessor(Node node) {int ws &#61; node.waitStatus;if (ws <0)compareAndSetWaitStatus(node, ws, 0);Node s &#61; node.next;if (s &#61;&#61; null || s.waitStatus > 0) {s &#61; null;for (Node t &#61; tail; t !&#61; null && t !&#61; node; t &#61; t.prev)if (t.waitStatus <&#61; 0)s &#61; t;}//如果有后继节点&#xff0c;就通过unpark来释放被挂起的线程if (s !&#61; null)LockSupport.unpark(s.thread);}

内部首先会发生的动作是获取head节点的next节点&#xff0c;如果获取到的节点不为空&#xff0c;则直接通过&#xff1a;“LockSupport.unpark()”方法来释放对应的被挂起的线程&#xff0c;这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁。

以上ReentrantLock的释放锁的过程就分析完毕了。


推荐阅读
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了Java集合库的使用方法,包括如何方便地重复使用集合以及下溯造型的应用。通过使用集合库,可以方便地取用各种集合,并将其插入到自己的程序中。为了使集合能够重复使用,Java提供了一种通用类型,即Object类型。通过添加指向集合的对象句柄,可以实现对集合的重复使用。然而,由于集合只能容纳Object类型,当向集合中添加对象句柄时,会丢失其身份或标识信息。为了恢复其本来面貌,可以使用下溯造型。本文还介绍了Java 1.2集合库的特点和优势。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
author-avatar
活宝贝aaaaa日记_452
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有