提交 4dbf9259 编写于 作者: 沉默王二's avatar 沉默王二 💬

AQS

上级 1d1dbced
......@@ -253,8 +253,8 @@
- [Java的内存模型(JMM)](docs/thread/jmm.md)
- [volatile关键字解析](docs/thread/volatile.md)
- [synchronized关键字解析](docs/thread/synchronized-1.md)
- [synchronized锁的到底是什么?](docs/thread/synchronized.md)
- [JDK15 移除了偏向锁](docs/thread/pianxiangsuo.md)
- [synchronized的四种锁状态](docs/thread/synchronized.md)
- [深入浅出偏向锁](docs/thread/pianxiangsuo.md)
- [CAS详解](docs/thread/cas.md)
- [AQS详解](docs/thread/aqs.md)
- [锁分类及 JUC 包下的那些锁](docs/thread/lock.md)
......
......@@ -262,8 +262,8 @@ head:
- [Java的内存模型(JMM)](thread/jmm.md)
- [volatile关键字解析](thread/volatile.md)
- [synchronized关键字解析](thread/synchronized-1.md)
- [synchronized锁的到底是什么?](thread/synchronized.md)
- [JDK15 移除了偏向锁](thread/pianxiangsuo.md)
- [synchronized的四种锁状态](thread/synchronized.md)
- [深入浅出偏向锁](thread/pianxiangsuo.md)
- [CAS详解](thread/cas.md)
- [AQS详解](thread/aqs.md)
- [锁分类及 JUC 包下的那些锁](thread/lock.md)
......
......@@ -20,7 +20,7 @@ head:
- 小册名字:二哥的 Java 进阶之路
- 小册作者:沉默王二
- 小册品质:能在 GitHub 取得 9000+ star 自认为品质是有目共睹的,尤其是国内还有不少小伙伴在访问 GitHub 的时候很不顺利。
- 小册品质:能在 GitHub 取得 9200+ star,品质可以说是有目共睹,尤其是国内还有不少小伙伴在访问 GitHub 的时候很不顺利。
- 小册风格:通俗易懂、风趣幽默、深度解析,新手可以拿来入门,老手可以拿来进阶,重要的知识,比如说面试高频的内容会从应用到源码挖个底朝天,还会穿插介绍一些计算机底层知识,力求讲个明白)
- 小册简介:这是一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java面试等核心知识点。学Java,就认准二哥的Java进阶之路😄
- 小册品位:底部用了梵高 1889 年的《星空》(the starry night),绝美的漩涡星空,耀眼的月亮,宁静的村庄,还有一颗燃烧着火焰的巨大柏树,我想小册的艺术品位也是恰到好处的。
......
......@@ -9,20 +9,20 @@ tag:
head:
- - meta
- name: keywords
content: Java,并发编程,多线程,Thread,AQS
content: Java,并发编程,多线程,Thread,AQS,抽象队列同步器
---
# 第十三节:抽象队列同步器 AQS
**AQS**`AbstractQueuedSynchronizer`的简称,即`抽象队列同步器`,从字面意思上理解:
**AQS**`AbstractQueuedSynchronizer`的简称,即`抽象队列同步器`,从字面上可以这样理解:
- 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
- 队列:使用先进先出(FIFO)队列存储数据;
- 队列:使用先进先出(FIFO)队列存储数据;
- 同步:实现了同步的功能。
那 AQS 有什么用呢?
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,比如我们提到的 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html),Semaphore,[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html),SynchronousQueue,[FutureTask](https://javabetter.cn/thread/callable-future-futuretask.html) 等等皆是基于 AQS 的。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,比如我们后面会细讲的 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html),Semaphore,[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html),SynchronousQueue,[FutureTask](https://javabetter.cn/thread/callable-future-futuretask.html) 等等,都是基于 AQS 的。
当然了,我们也可以利用 AQS 轻松定制专属的同步器,只要实现它的几个`protected`方法就可以了。
......@@ -45,26 +45,26 @@ setState()
compareAndSetState()
```
这三种操作均是原子操作,其中 compareAndSetState 的实现依赖于 Unsafe`compareAndSwapInt()` 方法。
这三种操作均是原子操作,其中 compareAndSetState 的实现依赖于 [Unsafe](https://javabetter.cn/thread/Unsafe.html)`compareAndSwapInt()` 方法。
AQS 内部使用了一个先进先出(FIFO)的[双端队列](https://javabetter.cn/collection/arraydeque.html),并使用了两个指针 head 和 tail 用于标识队列的头部和尾部。其数据结构如下图所示:
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/aqs-c294b5e3-69ef-49bb-ac56-f825894746ab.png)
但它并不直接储存线程,而是储存拥有线程的 Node 节点。
但它并不直接储存线程,而是储存拥有线程的 Node 节点。
![](https://cdn.tobebetterjavaer.com/stutymore/aqs-20230805211157.png)
## 资源共享模式
## AQS 的 Node 节点
资源有两种共享模式,或者说两种同步方式:
- 独占模式(Exclusive):资源是独占的,一次只能有一个线程获取。如 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)
- 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如 Semaphore/[CountDownLatch](https://javabetter.cn/thread/CountDownLatch.html)
- 独占模式(Exclusive):资源是独占的,一次只能有一个线程获取。如 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)(后面会细讲,戳链接直达)
- 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如 [Semaphore/CountDownLatch](https://javabetter.cn/thread/CountDownLatch.html)(戳链接直达,后面会细讲)
一般情况下,子类只需要根据需求实现其中一种模式就可以,当然也有同时实现两种模式的同步类,如`ReadWriteLock`
一般情况下,子类只需要根据需求实现其中一种模式就可以,当然也有同时实现两种模式的同步类,如 [ReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html)
AQS 中关于这两种资源共享模式的定义源码(均在内部类 Node 中)。我们来看看 Node 的结构:
AQS 中关于这两种资源共享模式的定义源码均在内部类 Node 中。我们来看看 Node 的结构:
```java
static final class Node {
......@@ -111,9 +111,44 @@ private Node addWaiter(Node mode) {
}
```
注意:通过 Node 我们可以实现两个队列,一是通过 prev 和 next 实现 CLH 队列(线程同步队列、双向队列),二是 nextWaiter 实现 Condition 条件上的等待线程队列(单向队列),这个 Condition 主要用在 ReentrantLock 类中。
通过 Node 我们可以实现两种队列:
## AQS 的主要源码解析
1)一是通过 prev 和 next 实现 CLH(Craig, Landin, and Hagersten)队列(线程同步队列、双向队列)。
在 CLH 锁中,每个等待的线程都会有一个关联的 Node,每个 Node 有一个 prev 和 next 指针。当一个线程尝试获取锁并失败时,它会将自己添加到队列的尾部并自旋,等待前一个节点的线程释放锁。类似下面这样。
```java
public class CLHLock {
private volatile Node tail;
private ThreadLocal<Node> myNode = ThreadLocal.withInitial(Node::new);
private ThreadLocal<Node> myPred = new ThreadLocal<>();
public void lock() {
Node node = myNode.get();
node.locked = true;
// 把自己放到队尾,并取出前面的节点
Node pred = tail;
myPred.set(pred);
while (pred.locked) {
// 自旋等待
}
}
public void unlock() {
Node node = myNode.get();
node.locked = false;
myNode.set(myPred.get());
}
private static class Node {
private volatile boolean locked;
}
}
```
2)二是通过 nextWaiter 实现 [Condition](https://javabetter.cn/thread/condition.html)(后面会细讲,戳链接直达)上的等待线程队列(单向队列),这个 Condition 主要用在 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html) 类中。
## AQS 的源码解析
AQS 的设计是基于**模板方法模式**的,它有一些方法必须要子类去实现的,它们主要有:
......@@ -131,7 +166,7 @@ protected boolean tryAcquire(int arg) {
}
```
这里不使用抽象方法的目的是:避免强迫子类中把所有的抽象方法都实现一遍,减少无用功,这样子类只需要实现自己关心的抽象方法即可,比如 Semaphore 只需要实现 tryAcquire 方法而不用实现其余不需要用到的模版方法:
这里不使用抽象方法的目的是:避免强迫子类中把所有的抽象方法都实现一遍,减少无用功,这样子类只需要实现自己关心的抽象方法即可,比如 [信号 Semaphore](https://javabetter.cn/thread/CountDownLatch.html) 只需要实现 tryAcquire 方法而不用实现其余不需要用到的模版方法:
![](https://cdn.tobebetterjavaer.com/stutymore/aqs-20230805211732.png)
......@@ -287,10 +322,84 @@ private void unparkSuccessor(Node node) {
## 小结
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,比如我们提到的 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)Semaphore[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html),SynchronousQueue,[FutureTask](https://javabetter.cn/thread/callable-future-futuretask.html) 等等皆是基于 AQS 的。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,比如我们提到的 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)[Semaphore](https://javabetter.cn/thread/CountDownLatch.html)[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html),SynchronousQueue,[FutureTask](https://javabetter.cn/thread/callable-future-futuretask.html) 等等皆是基于 AQS 的。
当然了,我们也可以利用 AQS 轻松定制专属的同步器,只要实现它的几个`protected`方法就可以了。
来个互斥锁(同一时刻只允许一个线程持有锁)。
```java
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class Mutex {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
```
上面的 Mutex 类是一个互斥锁。它内部使用了一个 Sync 类,该类继承自 AQS。
- tryAcquire:尝试获取资源。如果当前状态为0(未锁定),那么设置为1(锁定),并设置当前线程为独占资源的线程。
- tryRelease:尝试释放资源。设置状态为0并清除持有资源的线程。
- isHeldExclusively:判断当前资源是否被独占。
假设有一个线程不安全的资源,我们需要确保在任何时刻只有一个线程能访问它,那么就可以使用这个 Mutex 锁来确保线程安全。
```java
public class Resource {
private Mutex mutex = new Mutex();
public void use() {
mutex.lock();
try {
// 对资源的操作
} finally {
mutex.unlock();
}
}
}
```
在上述场景中,我们为一个不安全的资源添加了一个互斥锁,确保同一时刻只有一个线程可以使用这个资源,从而确保线程安全。
> 编辑:沉默王二,编辑前的内容来源于朋友开源的这个仓库:[深入浅出 Java 多线程](http://concurrent.redspider.group/),强烈推荐。值得参考文章:[君哥聊技术:2万字 + 40 张图带你精通 Java AQS](https://mp.weixin.qq.com/s/EWm7unc4lsXIv0iS3o12kg)
---
......
......@@ -14,17 +14,17 @@ head:
# 第十二节:乐观锁 CAS
CAS(Compare-and-Swap)是一种被广泛应用在并发控制中的算法,它是一种乐观锁的实现方式。CAS全称为“比较并交换”,是一种无锁的原子操作。
CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。
在并发编程中,我们都知道`i++`操作是非线程安全的,这是因为 `i++`操作不是原子操作,我们之前在讲[多线程带来了什么问题](https://javabetter.cn/thread/thread-bring-some-problem.html)中有讲到,大家应该还记得吧?
如何保证原子性呢?
常见的做法就是`加锁`
常见的做法就是加锁
在 Java 中,我们可以使用 [synchronized](https://javabetter.cn/thread/synchronized-1.html)关键字 和 `CAS`(Compare-and-Swap)来实现加锁效果。
`synchronized` 是悲观锁,尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多,但依然是悲观锁,这就意味着线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。
`synchronized` 是悲观锁,尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多[前面有细讲,戳链接回顾](https://javabetter.cn/thread/synchronized.html)),但依然是悲观锁,线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。
如果不好理解,我们来举个生活中的例子:一个人进入厕所后首先把门锁上(获取锁),然后开始上厕所,这个时候有其他人来了就只能在外面等(阻塞),就算再急也没用。上完厕所完事后把门打开(解锁),其他人就可以进入了。
......@@ -34,13 +34,13 @@ CAS(Compare-and-Swap)是一种被广泛应用在并发控制中的算法,
## 乐观锁与悲观锁
锁可以从不同的角度来分类。比如我们在前面讲[synchronized 到底锁的是什么](https://javabetter.cn/thread/synchronized.html)的时候,提到过偏向锁、轻量级锁、重量级锁,对吧?乐观锁和悲观锁也是一种分类方式。
锁可以从不同的角度来分类。比如我们在前面讲 [synchronized 四种锁状态](https://javabetter.cn/thread/synchronized.html)的时候,提到过偏向锁、轻量级锁、重量级锁,对吧?乐观锁和悲观锁也是一种分类方式。
**悲观锁:**
### 悲观锁
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
**乐观锁:**
### 乐观锁
乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。
......@@ -51,7 +51,7 @@ CAS(Compare-and-Swap)是一种被广泛应用在并发控制中的算法,
## 什么是 CAS
CAS 的全称是:比较并交换(Compare And Swap)。在 CAS 中,有这样三个值:
在 CAS 中,有这样三个值:
- V:要更新的变量(var)
- E:预期值(expected)
......@@ -78,11 +78,11 @@ CAS 的全称是:比较并交换(Compare And Swap)。在 CAS 中,有这
**当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。**
## Java 实现 CAS 的原理
## CAS 的原理
前面提到,CAS 是一种原子操作。那么 Java 是怎样来使用 CAS 的呢?我们知道,在 Java 中,如果一个[方法是 native 的](https://javabetter.cn/oo/native-method.html),那 Java 就不负责具体实现它,而是交给底层的 JVM 使用 C 语言 或者 C++ 去实现。
在 Java 中,有一个`Unsafe`类,它在`sun.misc`包中。它里面都是一些`native`方法,其中就有几个是关于 CAS 的:
在 Java 中,有一个`Unsafe`[后面会细讲,戳链接直达](https://javabetter.cn/thread/Unsafe.html),它在`sun.misc`包中。它里面都是一些`native`方法,其中就有几个是关于 CAS 的:
```java
boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
......@@ -94,9 +94,11 @@ Unsafe 对 CAS 的实现是通过 C++ 实现的,它的具体实现和操作系
Linux 的 X86 下主要是通过`cmpxchgl`这个指令在 CPU 上完成 CAS 操作的,但在多处理器情况下,必须使用`lock`指令加锁来完成。当然,不同的操作系统和处理器在实现方式上肯定会有所不同。
除了上面提到的方法,Unsafe 里面还有其它的方法。比如支持线程挂起和恢复的`park``unpark` 方法, [LockSupport 类](https://javabetter.cn/thread/LockSupport.html)底层就调用了这两个方法。还有支持[反射](https://javabetter.cn/basic-extra-meal/fanshe.html)操作的`allocateInstance()`方法。
>CMPXCHG是“Compare and Exchange”的缩写,它是一种原子指令,用于在多核/多线程环境中安全地修改共享数据。CMPXCHG在很多现代微处理器体系结构中都有,例如Intel x86/x64体系。对于32位操作数,这个指令通常写作CMPXCHG,而在64位操作数中,它被称为CMPXCHG8B或CMPXCHG16B。
除了上面提到的方法,Unsafe 里面还有其它的方法。比如支持线程挂起和恢复的`park``unpark` 方法, [LockSupport 类(后面会讲)](https://javabetter.cn/thread/LockSupport.html)底层就调用了这两个方法。还有支持[反射](https://javabetter.cn/basic-extra-meal/fanshe.html)操作的`allocateInstance()`方法。
## 具体如何实现的呢
## CAS 如何实现原子操作
上面介绍了 Unsafe 类的几个支持 CAS 的方法。那 Java 具体是如何通过这几个方法来实现原子操作的呢?
......@@ -104,16 +106,16 @@ JDK 提供了一些用于原子操作的类,在`java.util.concurrent.atomic`
![](https://cdn.tobebetterjavaer.com/stutymore/cas-20230731195315.png)
从名字就可以看出来这些类大概的用途:
从名字就可以看出来这些类大概的用途[原子类后面会细讲,戳链接直达](https://javabetter.cn/thread/atomic.html)
- 原子更新基本类型
- 原子更新数组
- 原子更新引用
- 原子更新字段(属性)
这里我们以`AtomicInteger`类的`getAndAdd(int delta)`方法为例,来看看 Java 是如何实现原子操作的,后面我们还会详细地讲其他的[原子类](https://javabetter.cn/thread/atomic.html)
这里我们以`AtomicInteger`类的`getAndAdd(int delta)`方法为例,来看看 Java 是如何实现原子操作的。
先来看 getAndAdd 方法的源码:
先来看 getAndAdd 方法的源码:
```java
public final int getAndAdd(int delta) {
......@@ -185,7 +187,7 @@ public final int getAndAddInt(Object o, long offset, int delta) {
## CAS 的三大问题
尽管 CAS 提供了一种有效的同步手段,但也存在一些问题,主要有以下三个:
尽管 CAS 提供了一种有效的同步手段,但也存在一些问题,主要有以下三个:ABA 问题、长时间自旋、多个共享变量的原子操作。
### ABA 问题
......@@ -227,7 +229,7 @@ public boolean compareAndSet(V expectedReference,
- 如果上述检查通过,也就是说当前的引用和标记与预期的相同,那么接下来就会检查新的引用和标记是否也与当前的相同。如果相同,那么实际上没有必要做任何改变,这个方法就会返回 true。
- 如果新的引用或者标记与当前的不同,那么就会调用 casPair 方法来尝试更新 pair 对象。casPair 方法会尝试用 newReference 和 newStamp 创建的新的 Pair 对象替换当前的 pair 对象。如果替换成功,casPair 方法会返回 true;如果替换失败(也就是说在尝试替换的过程中,pair 对象已经被其他线程改变了),casPair 方法会返回 false。
### 循环时间长、开销大
### 长时间自旋
CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。
......@@ -235,7 +237,7 @@ CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量
pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。
### 只能保证一个共享变量的原子操作
### 个共享变量的原子操作
当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:
......
此差异已折叠。
---
title: synchronized到底锁的什么?偏向锁、轻量级锁、重量级锁到底是什么?
shortTitle: 进击的synchronized
shortTitle: synchronized的四种锁状态
description: Java中的每一个对象都可以作为一个锁,这是synchronized实现同步的基础。当我们调用一个用synchronized关键字修饰的方法时,我们需要获取这个方法所在对象的锁。只有获取了这个锁,才可以执行这个方法。如果锁已经被其他线程获取,那么就会进入阻塞状态,直到锁被释放。
category:
- Java核心
......@@ -12,7 +12,7 @@ head:
content: Java,并发编程,多线程,Thread,synchronized,偏向锁,轻量级锁,重量级锁,锁
---
# 第十节:进击的synchronized
# 第十节:synchronized的四种锁状态
前面一节我们讲了 [synchronized 关键字的基本使用](https://javabetter.cn/thread/synchronized-1.html),它能用来同步方法和代码块,那 synchronized 到底锁的是什么呢?随着 JDK 版本的升级,synchronized 又做出了哪些改变呢?“synchronized 性能很差”的谣言真的存在吗?
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册