AQS是什么

AQS= volatile修饰的state变量(同步状态) +FIFO队列(CLH改善版的虚拟双向队列,用于阻塞等待唤醒机制)

队列里维护的Node节点主要包含:等待状态waitStatus,前后指针,等待的线程。

AQS是个抽象队列同步器,是JUC体系中用来构建锁和其他同步器如 ReentrantLock/CountDownLatch/Semphore的基石。AQS内部通过内置的FIFO先进先出的LCH(虚拟双向链表)队列来完成线程排队,并通过volatile 修饰的int类型状态变量来表示持有锁的状态。

简单的说,AQS通过volatile 修饰的int类型状态变量来表示同步状态,加volatial的目的是保证可见性。然后如果状态变量大于等于1是表示资源被占用,这时候抢不到资源的线程就要进入排队等候队列,等待资源的释放,这里面就需要阻塞等待唤醒机制来实现,AQS通过把等待获取资源的线程封装为Node<Thread>节点入队,在资源释放后通过LockSupport.park().unPark()来唤醒线程,通过CAS自旋来进行资源的抢占。

AQS源码解析——以ReentrantLock为例

公平锁与非公平锁

ReentrantLock默认是非公平锁,如果要实现公平锁构造函数中传入true表示创建的是公平锁。

公平锁相较于非公平锁体现在公平锁会先判断队列中是否有等待的线程,有的话优先获取到锁资源。

ReentrantLock 类图

AQS的Node属性含义

AQS结构图

非公平锁加锁过程

以3个线程分别为ABC争抢锁为例。

代码示例

public class ReentrantLockTest {
//模拟银行排队
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//第一个获取到锁的客户,执行自己的业务60秒
new Thread(() -> {
lock.lock();
try {
System.out.println("A 获取到锁,执行任务------------");
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
//第二个客户获取不到锁,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("B 获取到锁,执行任务------------");
} finally {
lock.unlock();
}
}, "B").start();
//第三个客户获取不到锁,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("C 获取到锁,执行任务------------");
} finally {
lock.unlock();
}
}, "C").start();
}
}
  1. 当A进来加锁时,会进行CAS加锁,加锁成功就会设置exclusiveOwnerThread目前占用锁的线程为自己。
  2. 当B进来加锁时,也会进行CAS尝试加锁,这时候加锁不成功后,或调用尝试获取锁的方法。这个方法里或再判断下这时候锁状态是否为0,也就是锁是否释放了,如果为0则会再进行CAS尝试加锁;如果锁状态不为0表示被占用了,这时候回判断是否目前加锁的线程是不是自己,如果是的话就进入可重入锁的逻辑,对加锁state变量加1,这就是我们加几次锁就要减几次锁的原因。
  3. 如果B尝试加锁失败后,B就会进入等待队列中进行等待,在加入队列的操作中,AQS会把线程B先包装成一个独占锁模式的Node节点,并判断尾结点是否为空,为空的话就要先判断队列是否还未初始化,如果还未初始化,会先创建一个空的哨兵节点(也叫虚节点,主要作用是用来占位),再将线程B的节点与哨兵节点进行双向队列关联,跟在哨兵节点后面,这时候就入队成功了。如果判断尾结点不为空,那就设置当前节点为尾结点,并与之前的尾结点设置关联关系。
  4. 在添加如队列成功后,线程B会调用acquireQueued方法继续尝试,线程B会通过自旋判断自己在队列中的位置,如果线程B的前节点是哨兵节点,那么线程B进行自旋处理,首先会继续CAS尝试加锁,这时候如果还是不成功,就会设置线程B的前缀节点的等待状态从0变成-1,表示等待被唤醒状态。继续进入自旋逻辑,还是会再尝试CAS尝试加锁一次,还是失败就会调用LockSupport.park(this);方法把线程设置为阻塞状态,等待被唤醒。
  5. 当线程A接收完业务后释放锁,释放锁时当判断释放后state的状态为0时,就会把当前锁的状态设置为0,表示锁已经空闲了,并设置exclusiveOwnerThread目前占用锁的线程为null。然后判断头结点是否不为空且头节点的等待状态为-1等待被唤醒,如果是的话就走唤醒逻辑,先把头节点等待状态设置为初始值0,然后判断头结点的后缀节点如不为空的话,就唤醒它。这样子线程B就会被唤醒了。
  6. 线程B被唤醒后就会继续进行自旋CAS尝试获取锁,这时候就能成功获取到了。而获取到锁后state状态继续变成1表示锁被占用,设置exclusiveOwnerThread目前占用锁的线程为线程B。然后把原来线程B的节点设置为头节点,并把B处理为null的哨兵节点,把原来的哨兵节点取消前后指针引用让GC回收掉。

注意

  • 一个线程会尝试抢4次锁才会进入到等待唤醒的阻塞状态中。

AQS为什么必须有哨兵节点——占位的目的

1.如果没有哨兵节点,那么每次执行入队操作,都需要判断head是否为空,如果为空则head=new Node如果不为空则head.next=new Node,而有哨兵节点则可以大胆的head.next=new Node.

2.如果没有哨兵节点,可能存在之前所说的安全性问题,当只有一个节点的时候执行入队方法,无法保证last和head不为空。哪怕执行enqueue入队之前last和head还指向一个节点,可能由于并发性在具体调用enqueue方法操作last的时候head和last共同指向的头节点已经完成出队,此时last和head都为null,所以enqueue方法中的last.next=new node会抛空指针异常,且由于线程并发性的问题,last始终可能随时为空的问题不使用哨兵节点是无法解决的

AQS的原理及源码分析的更多相关文章

  1. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  2. (转)ReentrantLock实现原理及源码分析

    背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...

  3. OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波

    http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...

  4. ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对Ha ...

  5. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  6. 【转】HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

  7. 【OpenCV】SIFT原理与源码分析:DoG尺度空间构造

    原文地址:http://blog.csdn.net/xiaowei_cqu/article/details/8067881 尺度空间理论   自然界中的物体随着观测尺度不同有不同的表现形态.例如我们形 ...

  8. 《深入探索Netty原理及源码分析》文集小结

    <深入探索Netty原理及源码分析>文集小结 https://www.jianshu.com/p/239a196152de

  9. HashMap实现原理及源码分析之JDK8

    继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap  基于JDK8的HashMap源码解析  [jdk1.8]HashMap源码分析 一.H ...

随机推荐

  1. Linux搭建rsync备份服务器备份

    环境: 1台rsync备份服务器,IP:10.0.0.188 1台rsync备份客户端,IP:10.0.0.51 备份数据需注意: 1. 在业务低谷时间进行备份 2. 进行备份限速 一.搭建rsync ...

  2. 50. Plus One-Leetcode

    Plus One My Submissions QuestionEditorial Solution Total Accepted: 98403 Total Submissions: 292594 D ...

  3. centos 安装reids

    1.安装tcl支持 yum install tcl 2.安装redis我们以最新的2.8.9为例 $ wget http://download.redis.io/releases/redis-2.8. ...

  4. 8种Vue中数据更新了但页面没有更新的情况

    目录 1.Vue 无法检测实例被创建时不存在于 data 中的 属性 2. Vue 无法检测'对象属性'的添加或移除 3.Vue 不能检测利用数组索引直接修改一个数组项 4.Vue 不能监测直接修改数 ...

  5. SpringBoot整合Shiro 四:认证+授权

    搭建环境见: SpringBoot整合Shiro 一:搭建环境 shiro配置类见: SpringBoot整合Shiro 二:Shiro配置类 shiro整合Mybatis见:SpringBoot整合 ...

  6. 从源码看Thread&ThreadLocal&ThreadLocalMap的关系与原理

    1.三者的之间的关系 ThreadLocalMap是Thread类的成员变量threadLocals,一个线程拥有一个ThreadLocalMap,一个ThreadLocalMap可以有多个Threa ...

  7. 简化版chmod

    我们知道对文件访问权限的修改在Shell下可通过chmod来进行 例如 可以看到v.c文件从无权限到所有者可读可写可执行.群组和其他用户可读可执行 chmod函数原型 int chmod(const ...

  8. Oracle 创建 md5 加密函数

    使用 Oracle 的 utl_raw.DBMS_OBFUSCATION_TOOLKIT 可以获取 md5 加密字符串: select utl_raw.cast_to_raw(DBMS_OBFUSCA ...

  9. Mysql的表级锁

    我们首先需要知道的一个大前提是:mysql的锁是由具体的存储引擎实现的.所以像Mysql的默认引擎MyISAM和第三方插件引擎 InnoDB的锁实现机制是有区别的.可根据不同的场景选用不同的锁定机制. ...

  10. JDBC(3):PreparedStatement对象介绍

    一,PreparedStatement介绍 PreperedStatement是Statement的子类,它的实例对象可以通过Connection.preparedStatement()方法获得,相对 ...