文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java高并发编程基础之AQS

2024-12-03 10:08

关注

 引言

曾经有一道比较比较经典的面试题“你能够说说java的并发包下面有哪些常见的类?”大多数人应该都可以说出 CountDownLatch、CyclicBarrier、Sempahore多线程并发三大利器。这三大利器都是通过AbstractQueuedSynchronizer抽象类(下面简写AQS)来实现的,所以学习三大利器之前我们有必要先来学习下AQS。

AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架”

AQS结构

说到同步我们如何来保证同步?大家第一印象肯定是加锁了,说到锁的话大家肯定首先会想到的是Synchronized。Synchronized大家应该基本上都会使用,加锁和释放锁都是jvm 来帮我们实现的,我们只需要简单的加个 Synchronized关键字就可以了。用起来超级方便。但是有没有一种情况我们设置一个锁的超时时间Synchronized就有点实现不了,这时候我们就可以用ReentrantLock来实现,ReentrantLock是通过aqs来实现的,今天我们就通过ReentrantLock来学习一下aqs。

CAS && 公平锁和非公平锁

AQS里面用到了大量的CAS学习AQS之前我们还是有必要简单的先了解下CAS、公平锁和非公平锁。

CAS

公平锁和非公平锁

  1. // 头节点 
  2. private transient volatile Node head; 
  3.  
  4. // 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表 
  5. private transient volatile Node tail; 
  6.  
  7. // 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁 
  8. // 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1 
  9. private volatile int state; 
  10.  
  11. // 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入 
  12. // reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁 
  13. // if (currentThread == getExclusiveOwnerThread()) {state++} 
  14. private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer 

下面我们来写一个demo分析下lock 加锁和释放锁的过程

  1. final void lock() { 
  2.            // 上来先试试直接把状态置位1,如果此时没人获取锁就直接 
  3.            if (compareAndSetState(0, 1)) 
  4.                 // 争抢成功则修改获得锁状态的线程 
  5.                setExclusiveOwnerThread(Thread.currentThread()); 
  6.            else 
  7.                acquire(1); 
  8.        } 

cas尝试失败,说明已经有人再持有锁,所以进入acquire方法

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

tryAcquire方法,看名字大概能猜出什么意思,就是试一试。tryAcquire实际上是调用了父类Sync的nonfairTryAcquire方法

  1. final boolean nonfairTryAcquire(int acquires) { 
  2.           final Thread current = Thread.currentThread(); 
  3.            // 获取下当前锁的状态 
  4.           int c = getState(); 
  5.           // 这个if 逻辑跟前面一进来就获取锁的逻辑一样都是通过cas尝试获取下锁 
  6.           if (c == 0) { 
  7.               if (compareAndSetState(0, acquires)) { 
  8.                   setExclusiveOwnerThread(current); 
  9.                   return true
  10.               } 
  11.           } 
  12.           // 进入这个判断说明 锁重入了 状态需要进行+1 
  13.           else if (current == getExclusiveOwnerThread()) { 
  14.               int nextc = c + acquires; 
  15.                // 如果锁的重入次数大于int的最大值,直接就抛出异常了,正常情况应该不存在这种情况,不过jdk还是严谨的 
  16.               if (nextc < 0) // overflow 
  17.                   throw new Error("Maximum lock count exceeded"); 
  18.               setState(nextc); 
  19.               return true
  20.           } 
  21.           // 返回false 说明尝试获取锁失败了,失败了就要进行acquireQueued方法了 
  22.           return false
  23.       } 

tryAcquire方法如果获取锁失败了,那么肯定就要排队等待获取锁。排队的线程需要待在哪里等待获取锁?这个就跟我们线程池执行任务一样,线程池把任务都封装成一个work,然后当线程处理任务不过来的时候,就把任务放到队列里面。AQS同样也是类似的,把排队等待获取锁的线程封装成一个NODE。然后再把NODE放入到一个队列里面。队列如下所示,不过需要注意一点head是不存NODE的。


 

 

接下来我们继续分析源码,看下获取锁失败是如何被加入队列的。就要执行acquireQueued方法,执行acquireQueued方法之前需要先执行addWaiter方法

  1. private Node addWaiter(Node mode) { 
  2.        Node node = new Node(Thread.currentThread(), mode); 
  3.        // Try the fast path of enq; backup to full enq on failure 
  4.        Node pred = tail; 
  5.        if (pred != null) { 
  6.            node.prev = pred; 
  7.            // cas 加入队列队尾 
  8.            if (compareAndSetTail(pred, node)) { 
  9.                pred.next = node; 
  10.                return node; 
  11.            } 
  12.        } 
  13.        // 尾结点不为空 || cas 加入尾结点失败 
  14.        enq(node); 
  15.        return node; 
  16.    } 

enq

接下来再看看enq方法

  1. // 通过自旋和CAS一定要当前node加入队尾 
  2. private Node enq(final Node node) { 
  3.         for (;;) { 
  4.             Node t = tail; 
  5.             // 尾结点为空说明队列还是空的,还没有被初始化,所以初始化头结点,可以看到头结点的node 是没有绑定线程的也就是不存数据的 
  6.             if (t == null) { // Must initialize 
  7.                 if (compareAndSetHead(new Node())) 
  8.                     tail = head; 
  9.             } else { 
  10.                 node.prev = t; 
  11.                 if (compareAndSetTail(t, node)) { 
  12.                     t.next = node; 
  13.                     return t; 
  14.                 } 
  15.             } 
  16.         } 
  17.     } 

通过addWaiter方法已经把获取锁的线程通过封装成一个NODE加入对列。上述方法的一个执行流程图如下:

接下来就是继续执行acquireQueued方法

 

acquireQueued

  1. final boolean acquireQueued(final Node node, int arg) { 
  2.     boolean failed = true
  3.     try { 
  4.         boolean interrupted = false
  5.         for (;;) { 
  6.              // 通过自旋去获取锁 前驱节点==head的时候去尝试获取锁,这个方法在前面已经分析过了。 
  7.             final Node p = node.predecessor(); 
  8.             if (p == head && tryAcquire(arg)) { 
  9.                 setHead(node); 
  10.                 p.next = null; // help GC 
  11.                 failed = false
  12.                 return interrupted; 
  13.             } 
  14.            // 进入这个if说明node的前驱节点不等于head 或者尝试获取锁失败了 
  15.            // 判断是否需要挂起当前线程 
  16.             if (shouldParkAfterFailedAcquire(p, node) && 
  17.                 parkAndCheckInterrupt()) 
  18.                 interrupted = true
  19.         } 
  20.     } finally { 
  21.            // 异常情况进入cancelAcquire,在jdk11的时候这个源码直接是catch (Throwable e){ cancelAcquire(node);} 简单明了 
  22.         if (failed) 
  23.             cancelAcquire(node); 
  24.     } 

setHead

这个方法每当有一个node获取到锁了,就把当前node节点设置为头节点,可以简单的看做当前节点获取到锁了就把当前节点”移除“(变为头结点)队列。

shouldParkAfterFailedAcquire

说到这个方法我们就要先看下NODE可能会有哪些状态在源码里面我们可以看到总共会有四种状态

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 
  2.         int ws = pred.waitStatus; 
  3.         // 前驱节点状态 如果这个状态为-1 则返回true,把当前线程挂起 
  4.         if (ws == Node.SIGNAL) 
  5.             return true
  6.         // 大于0,说明状态为CANCELLED  
  7.         if (ws > 0) { 
  8.             do { 
  9.                // 删除被取消的node(让被取消的node成为一个没有引用的node等着下次GC被回收) 
  10.                 node.prev = pred = pred.prev; 
  11.             } while (pred.waitStatus > 0); 
  12.             pred.next = node; 
  13.         } else { 
  14.             // 进入这里只能是 0,-2,-3。NODE节点初始化的时候waitStatus默认值是0,所以只有这里才有修改waitStatus的地方 
  15.             // 通过cas 把前驱节点的状态设置为-1,然后返回false ,外面调用这个方法的是个循环,又会调用一次这个方法 
  16.             compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
  17.         } 
  18.         return false
  19.     } 

parkAndCheckInterrupt

挂起当前线程,并且阻塞

  1. private final boolean parkAndCheckInterrupt() { 
  2.     LockSupport.park(this); // 挂起当前线程,阻塞 
  3.     return Thread.interrupted(); 

 

 


在这里插入图片描述

 

 

解锁

加锁成功了,那锁用完了就应该释放锁了,释放锁重点看下unparkSuccessor这个方法就好了

  1. private void unparkSuccessor(Node node) { 
  2.          // 头结点状态 
  3.        int ws = node.waitStatus; 
  4.        if (ws < 0) 
  5.            compareAndSetWaitStatus(node, ws, 0); 
  6.        Node s = node.next
  7.        // s==null head的successor节点获取锁成功后,执行了head.next=null的操作后,解锁线程读取了head.next,因此s==null 
  8.        // head的successor节点被取消(cancelAcquire)时,执行了如下操作:successor.waitStatus=1 ; successor.next = successor; 
  9.        if (s == null || s.waitStatus > 0) { 
  10.            s = null
  11.            // 从尾节点开始往前找,找到最前面的非取消的节点 这里没有break 哦 
  12.            for (Node t = tail; t != null && t != node; t = t.prev) 
  13.                if (t.waitStatus <= 0) 
  14.                    s = t; 
  15.        } 
  16.        if (s != null
  17.             // 唤醒线程 ,唤醒的线程会从acquireQueued去获取锁 
  18.            LockSupport.unpark(s.thread); 
  19.    } 

释放锁代码比较简单,基本都写在代码注释里面了,流程如下:

这段代码里面有一个比较经典的面试题:如果头结点的下一个节点为空或者头结点的下一个节点的状态为取消的时候为什么要从后往前找,找到最前面非取消的节点?

 

总结

reentrantLock的获取锁和释放锁基本就讲完了,里面还涉及多比较多的细节,感兴趣的同学可以对着源码一行一行去debug试试。

适当的了解aqs才能更好的学习CountDownLatch、CyclicBarrier、Sempahore,因为这三个利器都是基于aqs来实现的。

本文转载自微信公众号「java金融」,可以通过以下二维码关注。转载本文请联系java金融公众号。

 

来源:java金融内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯