上一篇: Java并发之AQS原理解读(一)

前言

本文从源码角度分析AQS独占锁工作原理,并介绍ReentranLock如何应用。

独占锁工作原理

独占锁即每次只有一个线程可以获得同一个锁资源。

获取锁

  1. 尝试获取资源(修改state),成功则返回
  2. 资源不足的情况下,线程会被封装成Node写入阻塞队列,然后以CAS自旋地方式循环重试获取锁(当插入的结点是head的直接后继时尝试获取锁,否则进入阻塞,只有当其他线程释放锁或者调用当前节点线程的中断方法时,才会重试获取锁)
  3. 自旋获取锁成功后,会将当前节点设为队列头结点
  4. 如果自旋阶段发生了线程中断,在获取锁成功之后,会补偿主动调用一次 interrupt 方法。因为自旋时调用的是interrupted方法返回中断标识,调用完后会清除状态

源码分析:

/* 获取独占锁
* 1. tryAcquire 先尝试获取锁,如果成功直接返回;
* 2. 否则 addWaiter 初始化辅助头结点,并将新节点添加到阻塞队列;
* acquireQueued 如果新节点是 head 的直接后继则尝试获取锁,否则 LockSupport 阻塞当前线程,
* 直到被释放锁的线程唤醒或者发生线程中断,才会重新尝试获取锁(CAS自旋阶段);
* 3. 获取锁成功后,如果之前循环重试阶段发生线程中断,则会通过 selfInterrupt 将线程中断标志设为 true
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} // 将新节点插入到队尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果tail节点不为null时,直接尝试插入
if (pred != null) {
node.prev = pred;
// 修改tail变量的值为插入节点的地址,即让tail指向新插入的节点
// pred的值不变,还是原tail的地址
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// tail节点为null时,先初始化辅助头节点,再插入新节点
enq(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;
}
}
}
} // CAS自旋阶段
// 循环重试获取锁,不成功就阻塞,直到被其他释放锁线程唤醒或发生线程中断,方法返回自旋阶段是否发生线程中断
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
/*
* 如果新插入结点是 head 的直接后继,则尝试获取锁
* 获取成功,则将当前节点设为head,并改成 dummy node(假结点)
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,丢弃原先的 dummy node
failed = false;
return interrupted;
}
/*
* 获取锁失败后阻塞当前节点,直到其他线程释放锁或调用当前线程的线程中断
* 发生线程中断的情况时,会将 interrupted 设为 true,表示自旋阶段发生了线程中断
* shouldParkAfterFailedAcquire方法在前驱节点状态不为SIGNAL的情况下都会循环重试获取锁
*/
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
} // 根据前驱节点等待状态判断是否要阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点等待状态为SIGNAL时,在释放锁的时候会唤醒后继节点,
* 所以当前节点的线程可以阻塞自己
*/
return true;
if (ws > 0) {
/*
* 前驱节点等待状态为CANCELLED时,向前遍历
* 断开对 CANCELLED 状态结点引用,help gc
* 之后会回到循环重试获取锁
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/**
* 等待状态为0或者PROPAGATE(-3)时,设置前驱节点等待状态为SIGNAL,
* 之后会回到循环重试获取锁
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
} // 阻塞当前线程并返回线程的中断标识
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,可以通过 LockSupport.unpark 或 currentThread.interrupt 唤醒
LockSupport.park(this);
return Thread.interrupted();
}

释放锁

  1. 尝试释放资源(修改state),如果失败直接返回
  2. 成功的话,再唤醒阻塞队列中的下一个结点的线程。当前节点后继不符合时,会从队尾往前找

源码分析:

// 释放锁
public final Boolean release(int arg) {
// 释放资源
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
} // 获取并唤醒头结点后的一个待唤醒结点(waitStatus<=0)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
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);
}

ReentranLock实现

非公平锁含义在于拥有锁的线程释放锁的时候,当前尝试获取锁资源的线程可以和队列中的第一个等待线程竞争;而已经进入队列的线程只能按照先进先出的顺序获取锁,也就是公平锁的逻辑。

第一次获取锁时,只要锁还未被占用,非公平锁会先直接通过CAS抢占,而公平锁则会判断是否有其他结点先进入队列,没有的话才会尝试获取锁。

ReentranLock是可重入锁,可重入即获得锁的线程可以重复进入临界区。通过实现AbstractOwnableSynchronizer接口记录获取锁的线程,当线程重复进入临界区时state+1,退出临界区时state-1

非公平锁

// 先尝试 CAS 抢占锁,失败后再通过 acquire 尝试
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 当锁未被占用时,尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 已获得锁的线程支持重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

公平锁

// 不抢占,通过 acquire 尝试
final void lock() {
acquire(1);
} protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/**
* 当锁未被占用时,且阻塞队列中没有结点比当前节点更早开始等待时,才尝试获取锁
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 已获得锁的线程支持重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

下一篇 Java并发之AQS原理解读(三) 将介绍共享锁工作原理。

参考:

Java AQS源码解读

hasQueuedPredecessors源码分析

Java并发之AQS原理解读(二)的更多相关文章

  1. Java并发之AQS原理解读(三)

    上一篇:Java并发之AQS原理解读(二) 前言 本文从源码角度分析AQS共享锁工作原理,并介绍下使用共享锁的子类如何工作的. 共享锁工作原理 共享锁与独占锁的不同之处在于,获取锁和释放锁成功后,都会 ...

  2. Java并发之AQS原理解读(一)

    前言 本文简要介绍AQS以及其中两个重要概念:state和Node. AQS 抽象队列同步器AQS是java.util.concurrent.locks包下比较核心的类之一,包括AbstractQue ...

  3. Java并发之AQS原理剖析

    概述: AbstractQueuedSynchronizer,可以称为抽象队列同步器. AQS有独占模式和共享模式两种: 独占模式: 公平锁: 非公平锁: 共享模式: 数据结构: 基本属性: /** ...

  4. 并发之AQS原理(二) CLH队列与Node解析

    并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...

  5. 并发之AQS原理(三) 如何保证并发

    并发之AQS原理(三) 如何保证并发 1. 如何保证并发 AbstractQueuedSynchronizer 维护了一个state(代表了共享资源)和一个FIFO线程等待队列(多线程竞争资源被阻塞时 ...

  6. 并发之AQS原理(一) 原理介绍简单使用

    并发之AQS原理(一) 如果说每一个同步的工具各有各的强大,那么这个强大背后是一个相同的动力,它就是AQS. AQS是什么 AQS是指java.util.concurrent.locks包里的Abst ...

  7. 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...

  8. Java并发之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  9. Java并发之AQS详解(转)

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

随机推荐

  1. Echarts +ajax+JSONPObject 实现后台数据图表化

    显示效果 实现步骤: 1-引入e-char,jquery资源, 2-发起jsonp跨域请求, 3-服务器端返回return new JSONPObject(callback, value);数据 4- ...

  2. JSON数据的HTTP Header应该怎么标记?

    第一种 header('Content-type: application/json'); 另一种 header('Content-type: text/json');

  3. (数据科学学习手札126)Python中JSON结构数据的高效增删改操作

    本文示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 在上一期文章中我们一起学习了在Python ...

  4. 题解CF757B

    题目 题意:在 \(s\) 数组中找出尽可能多的数使得他们的最大公约数 \(>1\) 既然最大公约数 \(>1\),\(s\) 数组的值域是 \(1 \le s_i \le 10^5\), ...

  5. C++ //构造函数的分类及调用 //分类 // 按照参数分类 无参构造函数(默认构造) 有参构造函数 //按照类型分类 普通构造 拷贝构造

    1 //构造函数的分类及调用 2 //分类 3 // 按照参数分类 无参构造函数(默认构造) 有参构造函数 4 //按照类型分类 普通构造 拷贝构造 5 6 #include <iostream ...

  6. 强烈IDEA这些插件,让你的开发速度飞起来!

    大家好,我是大彬~ 俗话说:工欲善其事必先利其器.今天给大家介绍几款我自己经常用的 IDEA 插件,很强大,助力大家开发. 插件安装 以IDEA为例,进入settings->Plugins-&g ...

  7. 刷到血赚!字节跳动内部出品:722页Android开发《360°全方面性能调优》学习手册首次外放,附项目实战!

    前言 我们平时在使用软件的过程中是不是遇到过这样的情况:"这个 app 怎么还没下载完!"."太卡了吧!"."图片怎么还没加载出来!".&q ...

  8. 在 CSS 中表示颜色的hex code方法和rgb方法

    hexadecimal code(十六进制编码),简写为 hex code. 我们通常使用 decimals,也就是十进制数字,它对每一位数字使用符号0到9来表示.Hexadecimals (或 he ...

  9. docker-01

    Docker介绍 1 什么是容器? Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移 ...

  10. 【XSS-labs】level 16-20

    Level 16 查看源码:发现 空格 / 和script都被转义了. 用img标签的onerror事件(在加载外部文档或图片出错后触发),空格用%0a绕过,  payload如下: <img% ...