深入java并发包源码(一)简介
深入java并发包源码(二)AQS的介绍与使用
深入java并发包源码(三)AQS独占方法源码分析

AQS 的实现原理

学完用 AQS 自定义一个锁以后,我们可以来看一下刚刚使用过的方法的实现。

分析源码的时候会省略一些不重要的代码。

AQS 的实现是基于一个 FIFO 队列的,每一个等待的线程被封装成 Node 存放在等待队列中,头结点是空的,不存储信息,等待队列中的节点都是阻塞的,并且在每次被唤醒后都会检测自己的前一个节点是否为头结点,如果是头节点证明在这个线程之前没有在等待的线程,就尝试着去获取共享资源。

AQS 的继承关系

AQS 继承了AbstractOwnableSynchronizer,我们先分析一下这个父类。

public abstract class AbstractOwnableSynchronizerimplements java.io.Serializable {protected AbstractOwnableSynchronizer() { }/*** 独占模式下的线程*/private transient Thread exclusiveOwnerThread;/*** 设置线程,只是对线程的 set 方法*/protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}/*** 设置线程,对线程的 get 方法*/protected final Thread getExclusiveOwnerThread() {return exclusiveOwnerThread;}
}

父类非常简单,持有一个独占模式下的线程,然后就只剩下对这个线程的 get 和 set 方法。

AQS的内部类

AQS 是用链表队列来实现线程等待的,那么队列肯定要有节点,我们先从节点讲起。

Node 类,每一个等待的线程都会被封装成 Node 类

Node 的域

public class Node {int waitStatus;Node prev;Node next;Thread thread;Node nextWaiter;
}

waitStatus:等待状态

prev:前驱节点

next:后继节点

thread:持有的线程

nextWaiter:condiction 队列中的后继节点

Node 的 status

Node 的状态有四种:

  1. CANCELLED,值为 1,表示当前的线程被取消,被打断或者获取超时了
  2. SIGNAL,值为 -1,表示当前节点的后继节点包含的线程需要运行,也就是 unpark;
  3. CONDITION,值为 -2,表示当前节点在等待 condition,也就是在 condition 队列中;
  4. PROPAGATE,值为 -3,表示当前场景下后续的 acquireShared 能够得以执行;

取消状态的值是唯一的正数,也是唯一当排队排到它了也不要资源而是直接轮到下个线程来获取资源的

AQS 中的方法源码分析

acquire

这个方法执行了:

  • 使用我们实现的 tryAcquire 来尝试获得锁,如果获得了锁则退出
  • 如果没有获得锁就将线程添加到等待队列中
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

看到上面的 tryAcquire 返回 false 后就会调用 addWaiter 新建节点加入等待队列中。参数 EXCLUSIVE 是独占模式。

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// 拿到尾节点,如果尾节点是空则说明是第一个节点,就直接入队就好了Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 如果尾节点不是空的,则需要特殊方法入队enq(node);return node;
}

addWaiter 方法创建完节点后,调用 enq 方法,在循环中用 CAS 操作将新的节点入队。

因为可能会有多个线程同时设置尾节点,所以需要放在循环中不断的设置尾节点。

private Node enq(final Node node) {for (;;) {Node t = tail;// 查看尾节点if (t == null) { // Must initialize// 尾节点为空则设置为刚刚创建的节点if (compareAndSetHead(new Node()))tail = head;} else {// 尾节点node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

在这里,节点入队就结束了。

那么我们回来前面分析的方法,

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

刚刚分析完了 addWaiter 方法,这个方法返回了刚刚创建并且加入的队列。现在开始分析 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 会把除了 next 以外的数据清除setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 这个方法查看在获取锁失败以后是否中断,如果否的话就调用// parkAndCheckInterrupt 阻塞方法线程,等待被唤醒if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

acquireInterruptibly

因为很像所以顺便来看一下 acquireInterruptibly 所调用的方法:

private void doAcquireInterruptibly(int arg)throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}// 只有这一句有差别,获取失败了并且检测到中断位被设为 true 直接抛出异常if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

acquireNanos

再来看一下有限时间的,当获取超时以后会将节点 Node 的状态设为 cancel,设置为取消的用处在后面的 release 方法中会有体现。

private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; failed = false;return true;}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L)return false;if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

总结一下过程

release

这个方法首先去调用了我们实现的 tryRelease,当结果返回成功的时候,拿到头结点,调用 unparkSuccessor 方法来唤醒头结点的下一个节点。

public final boolean release(long 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.waitSatus;// 因为已经获取过锁,所以将状态设设为 0。失败也没所谓,说明有其他的线程把它设为0了if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** 一般来说头结点的下一个节点是在等待着被唤醒的,但是如果是取消的或者意外的是空的,* 则向后遍历直到找到没有被取消的节点* */Node s = node.next;// 为空或者大于 0,只有 cancel 状态是大于 0 的if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread);
}

参考文献

  • 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
  • 方腾飞.Java 并发编程的艺术 [M]. 机械工业出版社, 2015.
  • 深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理
  • 【JUC】JDK1.8源码分析之AbstractQueuedSynchronizer(二)
  • AbstractQueuedSynchronizer的介绍和原理分析

转载于:https://www.cnblogs.com/zjmeow/p/9972202.html

深入java并发包源码(三)AQS独占方法源码分析相关推荐

  1. java map集合遍历方法,Java的Map集合的三种遍历方法

    集合的一个很重要的操作---遍历,学习了三种遍历方法,三种方法各有优缺点~~ 1. package com.myTest.MapText; import java.util.Collection; i ...

  2. Java并发包中Semaphore的工作原理、源码分析及使用示例

    简介: 在多线程程序设计中有三个同步工具需要我们掌握,分别是Semaphore(信号量),countDownLatch(倒计数门闸锁),CyclicBarrier(可重用栅栏) 欢迎探讨,如有错误敬请 ...

  3. Java里阻塞线程的三种实现方法

    在日常开发中,我们有时会遇到遇到多线程处理任务的情况,JDK里提供了便利的 ThreadPoolExecutor以及其包装的工具类Executors.但是我们知道 ExecutorService.ex ...

  4. Linux系统yum源的三种配置方法

    一.yum简述 yum是"Yellow dog Updater, Modified"的缩写,是一个软件包管理器当我们使用Linux操作系统时,我们绕不开的还是如何去下载软件,源码软 ...

  5. java字符串是否相等的三种判断方法

    1. == 比较的是否是同一对象 eg:String str1="abc",str2="abc" : if(str1==str2){} 结果为true 因为在j ...

  6. java全栈系列之JavaSE--数组的三种初始化方法及内存分析024

    java内存分析 堆中存放new出来的对象和数组,存放具体的值的变量存放在栈里面 在定义和创建数组的时候内存发生了什么? 当用户定义了一个数组,例如:int [ ] Array;只是定义了一个数组没有 ...

  7. java 释放数组_java集合ArrayList中clear方法内存释放分析

    最近在看ArrayList源码的时候看到了ArrayList的clear方法,源码如下: public void clear() { modCount++; // clear to let GC do ...

  8. JAVA要不要看源码_为什么要看源码、如何看源码,高手进阶必看

    作者:xybaby www.cnblogs.com/xybaby/p/10794700.html 由于项目的需求,最近花了较多的时间来看开源项目的代码,在本文中,简单总结一下对为什么要看源码.如何看源 ...

  9. 11没有源码注释_我们为什么要看源码、应该如何看源码?

    看源码的意义 看源码只是一种方法.手段,而不是目的.我也曾经给自己制定过"阅读xxx源码"的目标,现在看起来真的很蠢,一点不smart(specific.measurable.at ...

最新文章

  1. 【设计模式】装饰器模式类图和代码
  2. 聊聊JVM(六)理解JVM的safepoint
  3. 从Grunt测试Grunt插件
  4. gcc/g++基本命令简介
  5. 2017-2018-2 20179205 《网络攻防技术与实践》第八周作业
  6. java伪代码 读后感
  7. WPF设置控件获得焦点FocusManager
  8. Web安全攻防渗透测试实战指南笔记 三
  9. 计算机tpu定义,tpu材料
  10. Testbed软件下载安装使用试用
  11. Redis持久化策略AOF、RDB详解及源码分析
  12. 游戏评论之——戴森球计划
  13. 高省是什么?它跟社交电商APP有何区别?资深淘客为你揭秘
  14. Python中文件路径
  15. potplay显示服务器关闭,PotPlayer怎么关闭左上角显示播放时间?关掉左上角显示播放时间步骤一览...
  16. 12年计算机考研大纲,2012年计算机考研大纲解析之计算机组成原理
  17. 汇编语言课程视频目录 【B站】
  18. 大神教你如何搭建自己的web speedtest站点
  19. 师傅带徒弟学HTML+CSS-关东升-专题视频课程
  20. c语言简单课程设计报告,C语言课程设计报告—范例

热门文章

  1. netcore 内存限制_[翻译] 使用 Serverless 和 .NET Core 构建飞速发展的架构
  2. redis 使用-hiredis库使用(一) 基本篇 看完本文就可以上手工作了
  3. Django非常简单的安装方法
  4. python数据结构练习
  5. return 与 exit()的区别--return退出本函数,exit()退出整个程序
  6. 剑指offer 二维有序数组查找
  7. 机器学习模型在携程海外酒店推荐场景中的应用
  8. 用PMML实现机器学习模型的跨平台上线
  9. Java并发编程:volatile的使用
  10. 浅谈前端实现页面加载进度条以及 nprogress.js 的实现