AQS主要通过独占式、共享式同步状态的获取和释放,接下来我们来看下AQS代码是如何实现的

公平锁与非公平锁

  • 在公平锁的情况下,会先去判断线程是否需要排队,如果是的话会进行排队,不会进行抢锁操作,乖乖的进入队列进行排队,否则会进行抢锁操作
  • 在非公平锁的情况下,无论队列是否有线程在排队线程都会先去尝试获取同步状态,直接进行抢锁操作,成功则直接运行,否则进入到同步队列,后续的流程同公平锁一致

Node节点

在AQS中,是使用队列的方式来实现同步管理的,我们先来认识下队列中的Node节点数据结构

static final class Node {/*** 共享模式节点*/static final Node SHARED = new Node();/*** 独占模式节点*/static final Node EXCLUSIVE = null;/***  CANCELLED值,表明线程已取消*/static final int CANCELLED = 1;/*** SIGNAL值,表明后续线程需要释放*/static final int SIGNAL = -1;/*** CONDITION值,指示线程正在等待条件*/static final int CONDITION = -2;/*** PROPAGATE值,指示下一个acquireShared应该无条件传播*/static final int PROPAGATE = -3;/*** 等待状态*/volatile int waitStatus;/*** 上一个节点*/volatile Node prev;/*** 下一个节点*/volatile Node next;/*** 进入该节点队列的线程。构造时初始化,使用后为空。*/volatile Thread thread;/*** 条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量,* 也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段*/Node nextWaiter;/*** 是否为共享模式*/final boolean isShared() {return nextWaiter == SHARED;}// 返回当前节点的prev nodefinal Node predecessor() throws NullPointerException {Node p = prev;if (p == null) {throw new NullPointerException();} else {return p;}}Node() {    // 用于建立初始头或共享标记}Node(Thread thread, Node mode) { // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}
}

在Node节点中包含:

  • thread:一个等待获取同步状态的线程
  • prev:指向上一个节点的引用
  • next:指向下一个节点的引用
  • waitStatus:等待的状态
    • CANCELLED:值尾1,表示在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,接入CANCELLED状态后将不会再变化
    • SIGNAL:值尾-1,表示后续节点的线程处于等待状态,如果当前节点的线程释放了同步状态或者被取消便会通知后续节点,使后续节点的线程得以运行
    • CONDITION,值为-2 表示节点再条件队列中,节点等待线程再Condition上,当其它线程队Condition调用了signal()后,该节点将会从条件队列中转移到同步队列中
    • PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件传播下去
  • nextWaiter:表示条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量static final Node SHARED = new Node();,也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段
  • EXCLUSIVE:独占模式节点

同步队列结构


关键点:

  1. FIFO队列,先入先出
  2. 所有节点通过prev、next引用指向下一个节点
  3. 等待的节点通过LockSupport.park(this)来阻塞线程,让线程等待
  4. 当线程1执行完成后会唤醒后续等待的线程(状态需为SIGNAL,否则为CANCENEL则跳过)
    所有线程要获取锁都是通过state来进行判断:

    • 如果是等于0说明没有其它线程获取锁,可以被获取,获取后会设为1并将exclusiveOwnerThread设置为获取到锁的线程
    • 如果不是等于0则判断是否是exclusiveOwnerThread的线程,是的话则获取锁,state+1,否则进入等待队列进行排队

独占式同步状态的获取和释放

获取同步状态(acquire)

acquire 获取同步状态方法入口

    public final void acquire(int arg) {if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {// 设置当前线程的中断标识selfInterrupt();}}

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列自旋等待的相关工作,主要流程如下:

  1. 首先调用子类的tryAcquire方法,该方法是线程安全的获取同步状态,子类实现

  2. 如果获取失败,通过addWaiter方法构造独占式同步节点并将该节点加入到同步队列的尾部

     /*** 为代表当前线程并指定模式为mode的节点创建/进入队列。*/
    private Node addWaiter(Node mode) {// 将当前线程构造成Node节点Node node = new Node(Thread.currentThread(), mode);// 尝试快速在尾节点后新增节点 提升算法效率 先将尾节点指向predNode pred = tail;if (pred != null) {// 尾节点不为空 当前线程节点的前驱节点指向尾节点node.prev = pred; // 并发处理 尾节点有可能已经不是之前的节点 所以需要CAS更新if (compareAndSetTail(pred, node)) {// CAS更新成功 当前线程为尾节点 原先尾节点的后续节点就是当前节点pred.next = node; return node;}}//第一个入队的节点或者是尾节点后续节点新增失败时进入enqenq(node);return node;
    }private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) {//尾节点为空 第一次入队 设置头尾节点一致 同步队列的初始化if (compareAndSetHead(new Node()))tail = head;} else {//所有的线程节点在构造完成第一个节点后 依次加入到同步队列中node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
    }
    
  3. 然后调用acquireQueued方法,使得该节点以自旋的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠于前置节点的出队或阻塞线程被中断来实现

    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)) {//设置当前节点为头节点setHead(node);p.next = null; // help GCfailed = false;return interrupted;}//是否阻塞if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
    

    再来看看shouldParkAfterFailedAcquireparkAndCheckInterrupt方法是如何阻塞当前线程的

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//前驱节点的状态决定后续节点的行为int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** 前驱节点为-1 后续节点可以被阻塞*/return true;if (ws > 0) {/** 前置节点被取消了,跳过前置节点并继续重试,直到前置节点waitStatus<0,也就是不是取消状态的。*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** 前驱节点是初始或者共享状态就设置为-1 使后续节点阻塞*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
    }private final boolean parkAndCheckInterrupt() {//阻塞线程LockSupport.park(this);return Thread.interrupted();
    }
    

    节点自旋的过程大致示意图如下:

    在获取同步状态成功后,当前线程就会从acquire方法返回

释放锁(release)

public final boolean release(int arg) {if (tryRelease(arg)) {//同步状态释放成功Node h = head;if (h != null && h.waitStatus != 0)//直接释放头节点unparkSuccessor(h);return true;}return false;
}private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 没有后续节点或后续节点为CANCELLEDNode s = node.next;if (s == null || s.waitStatus > 0) {s = null;// 从尾节点开始向头节点遍历,遍历到整个队列最前排的waitStatus<=0的节点,赋值给s,用于后续的unpark操作。for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//唤醒后续节点执行自旋操作LockSupport.unpark(s.thread);
}

释放过程简单明了

独占式同步状态的获取和释放总结:

  1. 获取同步状态的时候,同步器维护一个同步队列
  2. 获取失败的线程会被加入队列中并进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态
  3. 在释放同步状态时,同步器调用tryRelease方法释放同步状态,然后唤醒头节点的后置节点

共享式同步状态的获取和释放

共享式的主要特点在于state的值,在初始化Sync的时候,会去设置AQS中的state值,state是多少就代表可以同时多少个线程获取该锁,比如实例化Semaphore的时候

    public Semaphore(int permits) {sync = new NonfairSync(permits);}static final class NonfairSync extends Sync {NonfairSync(int permits) {super(permits);}}abstract static class Sync extends AbstractQueuedSynchronizer {Sync(int permits) {// 设置state的初始值setState(permits);}}

获取同步状态(acquire)

public final void acquireShared(int arg) {//获取同步状态的返回值大于等于0时表示可以获取同步状态//小于0时表示可以获取不到同步状态  需要进入队列等待if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
}private void doAcquireShared(int arg) {// 和独占式一样的入队操作final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;// 自旋for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 前驱结点为头节点且成功获取同步状态 可退出自旋setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}private void setHeadAndPropagate(Node node, int propagate) {Node h = head; // 退出自旋的节点变成首节点setHead(node);if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {Node s = node.next;if (s == null || s.isShared())// 唤醒后续等待的节点doReleaseShared();}
}

释放同步状态(release)

与独占式一样,共享式获取也是需要释放同步状态,通过调用releaseShared方法可以释放同步状态,代码如下:

public final boolean releaseShared(int arg) {// 释放同步状态if (tryReleaseShared(arg)) {// 唤醒后续等待的节点doReleaseShared();return true;}return false;
}private void doReleaseShared() {// 自旋for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {continue;}// 唤醒后续节点unparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}if (h == head)break;}
}

unparkSuccessor方法和独占式是一样的。

共享锁和非共享锁的区别?

共享模式比独占模式多做了一步操作,就是调用了doReleaseShared方法,去唤醒队列中所有共享模式的节点,让这些线程在去争夺共享资源,而独占式是没有这个操作的。

后续

更多的AQS可以参考下一篇文章:ReentrantLock源码解析

Java 并发编程AQS--源码解读相关推荐

  1. Java并发编程-AQS源码之条件队列

    System.out.println(name + "==>成功获取到锁" + lock); try { condition.await(); } catch (Interr ...

  2. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  3. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  4. 【java】java 并发编程 CyclicBarrier 源码分析

    文章目录 1.概述 4.源码阅读 4.1 构造方法 4.2 Generation 4.3 属性Condition 4.4 await方法 4.4.1 BrokenBarrierException 4. ...

  5. 【java】java 并发编程 Condition 源码分析

    文章目录 1.概述 1.2 案例 3.2 实现方法顺序调用 2.源码解析 2.1 接口方法 2.2 继承 2.3 队列 2.4 await 分析 2.4.1 线程1 await 2.4.2 线程2 a ...

  6. 大厂高频面试题之Java并发核心AQS源码详解

    文章目录 什么是AQS AQS做了什么 AQS是如何保证并发安全的 父类 AbstractOwnableSynchronizer 主要成员变量 CLH队列 静态内部类Node waitStatus取值 ...

  7. Java并发编程 LockSupport源码分析

    这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. 1 package java.util.concurrent.loc ...

  8. Java Review - LinkedHashMap LinkedHashSet 源码解读

    文章目录 Pre 概述 数据结构 类继承关系 构造函数 方法 get() put() remove() LinkedHashSet 使用案例 - FIFO策略缓存 Pre Java Review - ...

  9. Java经典编程实例源码及视频专题汇总

    转:http://blog.itpub.net/28566218/viewspace-760945/ Java经典编程实例源码及视频专题汇总 Java是一种可以撰写跨平台应用软件的面向对象的程序设计语 ...

  10. aqs java 简书,Java AQS源码解读

    1.先聊点别的 说实话,关于AQS的设计理念.实现.使用,我有打算写过一篇技术文章,但是在写完初稿后,发现掌握的还是模模糊糊的,模棱两可. 痛定思痛,脚踏实地重新再来一遍.这次以 Java 8源码为基 ...

最新文章

  1. ZOJ 3820 Building Fire Stations
  2. Linux 系统的备份恢复
  3. GitHub 2W 星:一键生成前后端代码
  4. 农行校招考试计算机类,农行校招:还有4个月,流程是这样!
  5. Asp.net发送邮件的两种方法小结
  6. Android Resource介绍和使用
  7. 使用WebDriver遇到的那些坑
  8. Windows Phone 如何振动手机?
  9. 将类似html数据打印机,机器人和3D打印机的架构有哪些相似之处
  10. 面相终端的计算机网络的阶段特征,计算机网络习题汇编
  11. 苹果cmsV10资源采集插件
  12. Modown v4.11+Erphpdown10.01资源付费下载插件
  13. 微信小程序框架--weui
  14. MATLAB Codesys,CoDeSys学习日记(一)
  15. 最常见的管理误区,你中招了吗?
  16. GGSN与SGSN简介
  17. 【随便搞搞】自己写了一个用于炒股软件的自动选股分析代码 0603更新 天齐锂业两个板出局
  18. Ant Design中Form组件重置验证条件resetFields()方法
  19. 使用Java将阿拉伯数字转换为中文数字(适配小数转换)
  20. 2018沈阳网络赛J

热门文章

  1. python判断某年是否为闰年的程序_Python 学习--从0到1(4. 题3)
  2. java面试题2021最新
  3. resultmap拿不到数据_英雄联盟S10:半决赛数据告诉你huanfeng有多强
  4. Dom4j解析器_解析xml文档
  5. Struts2之初识篇(一)——与struts的区别和基本配置
  6. 批量生成10个虚拟主机配置
  7. 《1024伐木累》-程序员妹子与花木兰
  8. DecimalFormat的几种用法!关于字符串的使用
  9. 查找系列合集-二分查找
  10. Ruby Web实时消息后台服务器推送技术---GoEasy