返回顶部
首页 > 资讯 > 精选 >Java高并发编程基础之如何使用AQS
  • 256
分享到

Java高并发编程基础之如何使用AQS

2023-06-15 16:06:32 256人浏览 薄情痞子
摘要

本篇内容主要讲解“Java高并发编程基础之如何使用AQS”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java高并发编程基础之如何使用AQS”吧! 引言曾经有一道比较比较经典的面试题“

本篇内容主要讲解“Java高并发编程基础之如何使用AQS”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java高并发编程基础之如何使用AQS”吧!

 引言

曾经有一道比较比较经典的面试题“你能够说说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

  • CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数  :内存位置、预期数值和新值。CAS  的实现逻辑是将内存位置处的数值与预期数值相比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作,这个操作是个原子性操作,java里面的AtomicInteger等类都是通过cas来实现的。

公平锁和非公平锁

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,队列中第一个才能获得到锁。优点:等待锁的线程不会饿死,每个线程都可以获取到锁。缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必唤醒所有线程,会减少唤起线程的数量。缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。文字有点拗口,我们来个实际的例子说明下。比如我们去食堂就餐的时候都要排队,大家都按照先来后到的顺序排队打饭,这就是公平锁。如果等到你准备拿盘子打饭的时候  直接蹦出了一个五大三粗的胖子插队到你前面,你看打不赢他只能忍气吞声让他插队,等胖子打完饭了又来个小个子也来插你队,这时候你没法忍了,直接大吼一声让他滚,这个  小个子只能屁颠屁颠到队尾去排队了这就是非公平锁。我们先来看看AQS有哪些属性

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

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

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

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

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

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

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

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


Java高并发编程基础之如何使用AQS

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

private Node addWaiter(Node mode) {        Node node = new Node(Thread.currentThread(), mode);        // Try the fast path of enq; backup to full enq on failure        Node pred = tail;        if (pred != null) {            node.prev = pred;            // cas 加入队列队尾            if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        }        // 尾结点不为空 || cas 加入尾结点失败        enq(node);        return node;    }

enq

接下来再看看enq方法

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

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

Java高并发编程基础之如何使用AQS

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

acquireQueued

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说明node的前驱节点不等于head 或者尝试获取锁失败了            // 判断是否需要挂起当前线程             if (shouldParkAfterFailedAcquire(p, node) &&                 parkAndCheckInterrupt())                 interrupted = true;         }     } finally {            // 异常情况进入cancelAcquire,在jdk11的时候这个源码直接是catch (Throwable e){ cancelAcquire(node);} 简单明了         if (failed)             cancelAcquire(node);     } }

setHead

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

shouldParkAfterFailedAcquire

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

  • CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

  • CONDITioN:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

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

parkAndCheckInterrupt

挂起当前线程,并且阻塞

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

Java高并发编程基础之如何使用AQS


在这里插入图片描述

解锁

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

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

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

Java高并发编程基础之如何使用AQS

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

  • node.prev = pred; compareAndSetTail(pred, node)  这两个地方可以看作Tail入队的原子操作,但是此时pred.next =  node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。

  • 在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node

到此,相信大家对“Java高并发编程基础之如何使用AQS”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: Java高并发编程基础之如何使用AQS

本文链接: https://lsjlt.com/news/280948.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Java高并发编程基础之如何使用AQS
    本篇内容主要讲解“Java高并发编程基础之如何使用AQS”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java高并发编程基础之如何使用AQS”吧! 引言曾经有一道比较比较经典的面试题“...
    99+
    2023-06-15
  • 如何使用Java高并发编程之Semaphore
    本篇内容主要讲解“如何使用Java高并发编程之Semaphore”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何使用Java高并发编程之Semaphore”吧!共享锁、独占锁学习semapho...
    99+
    2023-06-15
  • 详解Java并发编程基础之volatile
    目录一、volatile的定义和实现原理1、Java并发模型采用的方式2、volatile的定义3、volatile的底层实现原理二、volatile的内存语义1、volatile的...
    99+
    2024-04-02
  • python基础之并发编程(一)
    目录一、进程(Process)二、线程(Thread)三、并发编程解决方案:四、多线程实现 (两种)1、第一种 函数方法2、第二种 类方法包装五、守护线程与子线程1、线程在分法有:2...
    99+
    2024-04-02
  • python基础之并发编程(二)
    目录一、多进程的实现方法一方法二:二、使用进程的优缺点1、优点2、缺点三、进程的通信1、Queue 实现进程间通信2、Pipe 实现进程间通信(一边发送send(obj),一边接收(...
    99+
    2024-04-02
  • python基础之并发编程(三)
    目录一、协程定义和作用1、使用协程的优点2、使用协程的缺点二、Greenlet 的使用三、Gevent的使用四、async io 异步 IO1、asyncio中的task的使用五、总...
    99+
    2024-04-02
  • 如何使用Java高并发编程CyclicBarrier
    本篇内容介绍了“如何使用Java高并发编程CyclicBarrier”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是CyclicBarr...
    99+
    2023-06-15
  • Java多线程之并发编程的核心AQS详解
    目录一、AQS简介1.1、AOS概念 1.2、AQS的核心思想1.3、AQS是自旋锁1.4、AQS支持两种资源分享的方式 二、AQS原理2.1、同步状态的管理2....
    99+
    2024-04-02
  • python基础之什么是并发编程
    本篇内容介绍了“python基础之什么是并发编程”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一、协程定义和作用协程(coroutine),...
    99+
    2023-06-25
  • Java并发编程之JUC并发核心AQS同步队列原理剖析
    目录一、AQS介绍二、AQS中的队列1、同步等待队列2、条件等待队列3、AQS队列节点Node三、同步队列源码分析1、同步队列分析2、同步队列——独占模式源码分析3、同步队列——共享...
    99+
    2024-04-02
  • 怎么用Java高并发编程之CountDownLatch
    本篇文章为大家展示了怎么用Java高并发编程之CountDownLatch,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。什么是CountDownLatchCountDownLatch是通过一个计数器...
    99+
    2023-06-15
  • Java并发编程之介绍线程安全基础的示例
    这篇文章主要介绍了Java并发编程之介绍线程安全基础的示例,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。线程安全基础线程安全问题账户取款案例同步代码块synchronized...
    99+
    2023-06-06
  • 详解Java高并发编程之AtomicReference
    目录一、AtomicReference 基本使用1.1、使用 synchronized 保证线程安全性二、了解 AtomicReference2.1、使用 AtomicReferen...
    99+
    2024-04-02
  • Java并发编程之CountDownLatch的使用
    目录前言基本使用await尝试获取锁获取锁失败countDown方法前言 CountDownLatch是一个倒数的同步器,和其他同步器不同的是,state为0时表示获取锁成功。常用来...
    99+
    2023-05-20
    Java并发编程CountDownLatch Java CountDownLatch使用 Java CountDownLatch
  • 《PHP并发编程基础教程:如何使用容器实现多线程编程?》
    PHP并发编程基础教程:如何使用容器实现多线程编程? PHP是一种广泛使用的服务器端编程语言,但是它在并发编程方面的表现并不理想。不过,使用容器技术,我们可以在PHP中实现多线程编程,从而提高程序的并发处理能力。本文将介绍如何使用容器技术来...
    99+
    2023-10-02
    并发 教程 容器
  • Java并发编程的基础知识有哪些
    今天小编给大家分享一下Java并发编程的基础知识有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。 01、简介首...
    99+
    2023-06-16
  • Java之JMM高并发编程实例分析
    这篇文章主要介绍“Java之JMM高并发编程实例分析”,在日常操作中,相信很多人在Java之JMM高并发编程实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java之JMM高并发编程实例分析”的疑惑有所...
    99+
    2023-07-02
  • Java并发编程之Semaphore的使用简介
    目录简介Semaphored的使用构造方法核心方法示例使用Semaphore实现互斥简介 Semaphore是用来限制访问特定资源的并发线程的数量,相对于内置锁synchroniz...
    99+
    2024-04-02
  • Java并发编程之Executor接口的使用
    一、Executor接口的理解 Executor属于java.util.concurrent包下; Executor是任务执行机制的核心接口; 二、Executo...
    99+
    2024-04-02
  • Java并发之BlockingQueue如何使用
    Java中的BlockingQueue是一个线程安全的队列实现,它支持在生产者和消费者之间进行数据交换。以下是BlockingQue...
    99+
    2023-08-12
    Java BlockingQueue
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作