mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-14 05:21:42 +08:00
Compare commits
2 Commits
c706bebb0d
...
211fc37e1d
Author | SHA1 | Date | |
---|---|---|---|
|
211fc37e1d | ||
|
acf10f6e03 |
@ -124,7 +124,7 @@ echo $hello
|
||||
输出内容:
|
||||
|
||||
```
|
||||
Hello, I am SnailClimb!
|
||||
Hello, I am '$name'!
|
||||
```
|
||||
|
||||
**双引号字符串:**
|
||||
|
@ -22,20 +22,20 @@ AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,
|
||||
|
||||
## AQS 原理
|
||||
|
||||
> 👍推荐阅读:[从 ReentrantLock 的实现看 AQS 的原理及应用](./reentrantlock.md)
|
||||
|
||||
在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
|
||||
|
||||
### AQS 核心思想
|
||||
|
||||
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 **CLH 队列锁** 实现的,即将暂时获取不到锁的线程加入到队列中。
|
||||
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 实现的。
|
||||
|
||||
CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
|
||||
CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
|
||||
|
||||
CLH 队列结构如下图所示:
|
||||
CLH 队列锁结构如下图所示:
|
||||
|
||||

|
||||
|
||||
关于AQS 核心数据结构-CLH 锁的详细解读,强烈推荐阅读 [Java AQS 核心数据结构-CLH 锁 - Qunar技术沙龙](https://mp.weixin.qq.com/s/jEx-4XhNGOFdCo4Nou5tqg) 这篇文章。
|
||||
|
||||
AQS(`AbstractQueuedSynchronizer`)的核心原理图(图源[Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html))如下:
|
||||
|
||||

|
||||
@ -66,7 +66,7 @@ protected final boolean compareAndSetState(int expect, int update) {
|
||||
}
|
||||
```
|
||||
|
||||
以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。
|
||||
以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。相关阅读:[从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队](./reentrantlock.md)。
|
||||
|
||||
再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
@ -33,7 +33,7 @@ implements List<E>, RandomAccess, Cloneable, Serializable
|
||||
|
||||
在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 `List` 的内部数据,毕竟读取操作是安全的。
|
||||
|
||||
这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?**
|
||||
这和我们之前提到过的 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?**
|
||||
|
||||
### CopyOnWriteArrayList 是如何做到的?
|
||||
|
||||
@ -173,7 +173,7 @@ private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueu
|
||||
|
||||
## ConcurrentSkipListMap
|
||||
|
||||
下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。
|
||||
> 下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。
|
||||
|
||||
为了引出 `ConcurrentSkipListMap`,先带着大家简单理解一下跳表。
|
||||
|
||||
|
@ -12,126 +12,6 @@ head:
|
||||
content: Java并发常见知识点和面试题总结(含详细解答)。
|
||||
---
|
||||
|
||||
## 乐观锁和悲观锁
|
||||
|
||||
### 什么是悲观锁?使用场景是什么?
|
||||
|
||||
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
|
||||
|
||||
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
|
||||
|
||||
像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。
|
||||
|
||||
**悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
|
||||
|
||||
### 什么是乐观锁?使用场景是什么?
|
||||
|
||||
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
|
||||
|
||||
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
|
||||
|
||||
**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
|
||||
|
||||
### 如何实现乐观锁?
|
||||
|
||||
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
|
||||
|
||||
#### 版本号机制
|
||||
|
||||
一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。
|
||||
|
||||
**举一个简单的例子** :假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。
|
||||
|
||||
1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。
|
||||
2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。
|
||||
3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。
|
||||
4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
|
||||
|
||||
这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
|
||||
|
||||
#### CAS 算法
|
||||
|
||||
CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
|
||||
|
||||
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
|
||||
|
||||
> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
|
||||
|
||||
CAS 涉及到三个操作数:
|
||||
|
||||
- **V** :要更新的变量值(Var)
|
||||
- **E** :预期值(Expected)
|
||||
- **N** :拟写入的新值(New)
|
||||
|
||||
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。
|
||||
|
||||
**举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
|
||||
|
||||
1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
|
||||
2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
|
||||
|
||||
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
|
||||
|
||||
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。
|
||||
|
||||
`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作
|
||||
|
||||
```java
|
||||
/**
|
||||
* CAS
|
||||
* @param o 包含要修改field的对象
|
||||
* @param offset 对象中某field的偏移量
|
||||
* @param expected 期望值
|
||||
* @param update 更新值
|
||||
* @return true | false
|
||||
*/
|
||||
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
|
||||
|
||||
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
|
||||
|
||||
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
|
||||
```
|
||||
|
||||
关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。
|
||||
|
||||
### 乐观锁存在哪些问题?
|
||||
|
||||
ABA 问题是乐观锁最常见的问题。
|
||||
|
||||
#### ABA 问题
|
||||
|
||||
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
|
||||
|
||||
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
|
||||
|
||||
```java
|
||||
public boolean compareAndSet(V expectedReference,
|
||||
V newReference,
|
||||
int expectedStamp,
|
||||
int newStamp) {
|
||||
Pair<V> current = pair;
|
||||
return
|
||||
expectedReference == current.reference &&
|
||||
expectedStamp == current.stamp &&
|
||||
((newReference == current.reference &&
|
||||
newStamp == current.stamp) ||
|
||||
casPair(current, Pair.of(newReference, newStamp)));
|
||||
}
|
||||
```
|
||||
|
||||
#### 循环时间长开销大
|
||||
|
||||
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
|
||||
|
||||
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
|
||||
|
||||
1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
|
||||
2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
|
||||
|
||||
#### 只能保证一个共享变量的原子操作
|
||||
|
||||
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
|
||||
|
||||
## JMM(Java 内存模型)
|
||||
|
||||
JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题: [JMM(Java 内存模型)详解](./jmm.md) 。
|
||||
@ -292,6 +172,126 @@ public void increase() {
|
||||
}
|
||||
```
|
||||
|
||||
## 乐观锁和悲观锁
|
||||
|
||||
### 什么是悲观锁?使用场景是什么?
|
||||
|
||||
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
|
||||
|
||||
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
|
||||
|
||||
像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。
|
||||
|
||||
**悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
|
||||
|
||||
### 什么是乐观锁?使用场景是什么?
|
||||
|
||||
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
|
||||
|
||||
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
|
||||
|
||||
**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
|
||||
|
||||
### 如何实现乐观锁?
|
||||
|
||||
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
|
||||
|
||||
#### 版本号机制
|
||||
|
||||
一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。
|
||||
|
||||
**举一个简单的例子** :假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。
|
||||
|
||||
1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。
|
||||
2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。
|
||||
3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。
|
||||
4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
|
||||
|
||||
这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
|
||||
|
||||
#### CAS 算法
|
||||
|
||||
CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
|
||||
|
||||
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
|
||||
|
||||
> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
|
||||
|
||||
CAS 涉及到三个操作数:
|
||||
|
||||
- **V** :要更新的变量值(Var)
|
||||
- **E** :预期值(Expected)
|
||||
- **N** :拟写入的新值(New)
|
||||
|
||||
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
|
||||
|
||||
**举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
|
||||
|
||||
1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
|
||||
2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
|
||||
|
||||
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
|
||||
|
||||
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。
|
||||
|
||||
`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作
|
||||
|
||||
```java
|
||||
/**
|
||||
* CAS
|
||||
* @param o 包含要修改field的对象
|
||||
* @param offset 对象中某field的偏移量
|
||||
* @param expected 期望值
|
||||
* @param update 更新值
|
||||
* @return true | false
|
||||
*/
|
||||
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
|
||||
|
||||
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
|
||||
|
||||
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
|
||||
```
|
||||
|
||||
关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。
|
||||
|
||||
### 乐观锁存在哪些问题?
|
||||
|
||||
ABA 问题是乐观锁最常见的问题。
|
||||
|
||||
#### ABA 问题
|
||||
|
||||
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
|
||||
|
||||
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
|
||||
|
||||
```java
|
||||
public boolean compareAndSet(V expectedReference,
|
||||
V newReference,
|
||||
int expectedStamp,
|
||||
int newStamp) {
|
||||
Pair<V> current = pair;
|
||||
return
|
||||
expectedReference == current.reference &&
|
||||
expectedStamp == current.stamp &&
|
||||
((newReference == current.reference &&
|
||||
newStamp == current.stamp) ||
|
||||
casPair(current, Pair.of(newReference, newStamp)));
|
||||
}
|
||||
```
|
||||
|
||||
#### 循环时间长开销大
|
||||
|
||||
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
|
||||
|
||||
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
|
||||
|
||||
1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
|
||||
2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
|
||||
|
||||
#### 只能保证一个共享变量的原子操作
|
||||
|
||||
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
|
||||
|
||||
## synchronized 关键字
|
||||
|
||||
### synchronized 是什么?有什么用?
|
||||
@ -300,7 +300,7 @@ public void increase() {
|
||||
|
||||
在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
|
||||
|
||||
不过,在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多。因此, `synchronized` 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 `synchronized` 。
|
||||
不过,在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多。因此, `synchronized` 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 `synchronized` 。
|
||||
|
||||
### 如何使用 synchronized?
|
||||
|
||||
@ -443,13 +443,60 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
||||
- `volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。
|
||||
- `volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。
|
||||
|
||||
## ReentrantLock
|
||||
|
||||
### ReentrantLock 是什么?
|
||||
|
||||
`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,和 `synchronized` 关键字类似。不过,`ReentrantLock` 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
|
||||
|
||||
```java
|
||||
public class ReentrantLock implements Lock, java.io.Serializable {}
|
||||
```
|
||||
|
||||
`ReentrantLock` 里面有一个内部类 `Sync`,`Sync` 继承 AQS(`AbstractQueuedSynchronizer`),添加锁和释放锁的大部分操作实际上都是在 `Sync` 中实现的。`Sync` 有公平锁 `FairSync` 和非公平锁 `NonfairSync` 两个子类。
|
||||
|
||||

|
||||
|
||||
`ReentrantLock` 默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
|
||||
|
||||
```java
|
||||
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
|
||||
public ReentrantLock(boolean fair) {
|
||||
sync = fair ? new FairSync() : new NonfairSync();
|
||||
}
|
||||
```
|
||||
|
||||
从上面的内容可以看出, `ReentrantLock` 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html) 这篇文章。
|
||||
|
||||
### 公平锁和非公平锁有什么区别?
|
||||
|
||||
- **公平锁** : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
|
||||
- **非公平锁** :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
|
||||
|
||||
### synchronized 和 ReentrantLock 有什么区别?
|
||||
|
||||
#### 两者都是可重入锁
|
||||
|
||||
**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
|
||||
**可重入锁** 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
|
||||
|
||||
**JDK 提供的所有现成的 `Lock` 实现类,包括 `synchronized` 关键字锁都是可重入的。**
|
||||
JDK 提供的所有现成的 `Lock` 实现类,包括 `synchronized` 关键字锁都是可重入的。
|
||||
|
||||
在下面的代码中,`method1()` 和 `method2()`都被 `synchronized` 关键字修饰,`method1()`调用了`method2()`。
|
||||
|
||||
```java
|
||||
public class ReentrantLockDemo {
|
||||
public synchronized void method1() {
|
||||
System.out.println("方法1");
|
||||
method2();
|
||||
}
|
||||
|
||||
public synchronized void method2() {
|
||||
System.out.println("方法2");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
由于 `synchronized`锁是可重入的,同一个线程在调用`method1()` 时可以直接获得当前对象的锁,执行 `method2()` 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如`synchronized`是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 `method2()`时获取锁失败,会出现死锁问题。
|
||||
|
||||
#### synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
|
||||
|
||||
@ -467,185 +514,150 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
||||
|
||||
如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。
|
||||
|
||||
关于公平锁和非公平锁的补充:
|
||||
|
||||
> - **公平锁** : 锁被释放之后,先申请的线程/进程先得到锁。
|
||||
> - **非公平锁** :锁被释放之后,后申请的线程/进程可能会先获取到锁,是随机或者按照其他优先级排序的。
|
||||
|
||||
关于 `Condition`接口的补充:
|
||||
|
||||
> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。
|
||||
|
||||
## ThreadLocal
|
||||
### 可中断锁和不可中断锁有什么区别?
|
||||
|
||||
### ThreadLocal 有什么用?
|
||||
- **可中断锁** :获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。`ReentrantLock` 就属于是可中断锁。
|
||||
- **不可中断锁** :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 `synchronized` 就属于是不可中断锁。
|
||||
|
||||
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?**
|
||||
## ReentrantReadWriteLock
|
||||
|
||||
JDK 中自带的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
|
||||
`ReentrantReadWriteLock` 在实际项目中使用的并不多,面试中也问的比较少,简单了解即可。JDK 1.8 引入了性能更好的读写锁 `StampedLock` 。
|
||||
|
||||
如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
|
||||
### ReentrantReadWriteLock 是什么?
|
||||
|
||||
再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
|
||||
|
||||
### 如何使用 ThreadLocal?
|
||||
|
||||
相信看了上面的解释,大家已经搞懂 `ThreadLocal` 类是个什么东西了。下面简单演示一下如何在项目中实际使用 `ThreadLocal` 。
|
||||
`ReentrantReadWriteLock` 实现了 `ReadWriteLock` ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
|
||||
|
||||
```java
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Random;
|
||||
|
||||
public class ThreadLocalExample implements Runnable{
|
||||
|
||||
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
|
||||
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
ThreadLocalExample obj = new ThreadLocalExample();
|
||||
for(int i=0 ; i<10; i++){
|
||||
Thread t = new Thread(obj, ""+i);
|
||||
Thread.sleep(new Random().nextInt(1000));
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
|
||||
try {
|
||||
Thread.sleep(new Random().nextInt(1000));
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//formatter pattern is changed here by thread, but it won't reflect to other threads
|
||||
formatter.set(new SimpleDateFormat());
|
||||
|
||||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
|
||||
}
|
||||
|
||||
public class ReentrantReadWriteLock
|
||||
implements ReadWriteLock, java.io.Serializable{
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
输出结果 :
|
||||
|
||||
```
|
||||
Thread Name= 0 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 0 formatter = yy-M-d ah:mm
|
||||
Thread Name= 1 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 2 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 1 formatter = yy-M-d ah:mm
|
||||
Thread Name= 3 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 2 formatter = yy-M-d ah:mm
|
||||
Thread Name= 4 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 3 formatter = yy-M-d ah:mm
|
||||
Thread Name= 4 formatter = yy-M-d ah:mm
|
||||
Thread Name= 5 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 5 formatter = yy-M-d ah:mm
|
||||
Thread Name= 6 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 6 formatter = yy-M-d ah:mm
|
||||
Thread Name= 7 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 7 formatter = yy-M-d ah:mm
|
||||
Thread Name= 8 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 9 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 8 formatter = yy-M-d ah:mm
|
||||
Thread Name= 9 formatter = yy-M-d ah:mm
|
||||
```
|
||||
|
||||
从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。
|
||||
|
||||
上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
|
||||
|
||||
```java
|
||||
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue(){
|
||||
return new SimpleDateFormat("yyyyMMdd HHmm");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### ThreadLocal 原理了解吗?
|
||||
|
||||
从 `Thread`类源代码入手。
|
||||
|
||||
```java
|
||||
public class Thread implements Runnable {
|
||||
//......
|
||||
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
|
||||
ThreadLocal.ThreadLocalMap threadLocals = null;
|
||||
|
||||
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
|
||||
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
|
||||
//......
|
||||
public interface ReadWriteLock {
|
||||
Lock readLock();
|
||||
Lock writeLock();
|
||||
}
|
||||
```
|
||||
|
||||
从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。
|
||||
- 一般锁进行并发控制的规则:读读互斥、读写互斥、写写互斥。
|
||||
- 读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)。
|
||||
|
||||
`ThreadLocal`类的`set()`方法
|
||||
`ReentrantReadWriteLock` 其实是两把锁,一把是 `WriteLock` (写锁),一把是 `ReadLock`(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
|
||||
|
||||
和 `ReentrantLock` 一样,`ReentrantReadWriteLock` 底层也是基于 AQS 实现的。
|
||||
|
||||

|
||||
|
||||
`ReentrantReadWriteLock` 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定。
|
||||
|
||||
```java
|
||||
public void set(T value) {
|
||||
//获取当前请求的线程
|
||||
Thread t = Thread.currentThread();
|
||||
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null)
|
||||
// 将需要存储的值放入到这个哈希表中
|
||||
map.set(this, value);
|
||||
else
|
||||
createMap(t, value);
|
||||
}
|
||||
ThreadLocalMap getMap(Thread t) {
|
||||
return t.threadLocals;
|
||||
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
|
||||
public ReentrantReadWriteLock(boolean fair) {
|
||||
sync = fair ? new FairSync() : new NonfairSync();
|
||||
readerLock = new ReadLock(this);
|
||||
writerLock = new WriteLock(this);
|
||||
}
|
||||
```
|
||||
|
||||
通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
|
||||
### ReentrantReadWriteLock 适合什么场景?
|
||||
|
||||
**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。**
|
||||
由于 `ReentrantReadWriteLock` 既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下,使用 `ReentrantReadWriteLock` 能够明显提升系统性能。
|
||||
|
||||
### 共享锁和独占锁有什么区别?
|
||||
|
||||
- **共享锁** :一把锁可以被多个线程同时获得。
|
||||
- **独占锁** :一把锁只能被一个线程获得。
|
||||
|
||||
### 线程持有读锁还能获取写锁吗?
|
||||
|
||||
- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
|
||||
- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
|
||||
|
||||
读写锁的源码分析,推荐阅读 [聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件 ](https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw) 这篇文章,写的很不错。
|
||||
|
||||
### 读锁为什么不能升级为写锁?
|
||||
|
||||
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为写锁降级为读锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
|
||||
|
||||
另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
|
||||
|
||||
## StampedLock
|
||||
|
||||
`StampedLock` 面试中问的比较少,不是很重要,简单了解即可。
|
||||
|
||||
### StampedLock 是什么?
|
||||
|
||||
`StampedLock` 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 `Conditon`。
|
||||
|
||||
不同于一般的 `Lock` 类,`StampedLock` 并不是直接实现 `Lock`或 `ReadWriteLock`接口,而是基于 **CLH 锁** 独立实现的(AQS 也是基于这玩意)。
|
||||
|
||||
```java
|
||||
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
|
||||
//......
|
||||
public class StampedLock implements java.io.Serializable {
|
||||
}
|
||||
```
|
||||
|
||||
比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话, `Thread`内部都是使用仅有的那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。
|
||||
`StampedLock` 提供了三种模式的读写控制模式:读锁、写锁和乐观读。
|
||||
|
||||
`ThreadLocal` 数据结构如下图所示:
|
||||
- **写锁**:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 `ReentrantReadWriteLock` 的写锁,不过这里的写锁是不可重入的。
|
||||
- **读锁** (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 `ReentrantReadWriteLock` 的读锁,不过这里的读锁是不可重入的。
|
||||
- **乐观读** :允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。
|
||||
|
||||

|
||||
|
||||
`ThreadLocalMap`是`ThreadLocal`的静态内部类。
|
||||
|
||||

|
||||
|
||||
### ThreadLocal 内存泄露问题是怎么导致的?
|
||||
|
||||
`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
|
||||
|
||||
这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
|
||||
另外,`StampedLock` 还支持这三种锁在一定条件下进行相互转换 。
|
||||
|
||||
```java
|
||||
static class Entry extends WeakReference<ThreadLocal<?>> {
|
||||
/** The value associated with this ThreadLocal. */
|
||||
Object value;
|
||||
long tryConvertToWriteLock(long stamp){}
|
||||
long tryConvertToReadLock(long stamp){}
|
||||
long tryConvertToOptimisticRead(long stamp){}
|
||||
```
|
||||
|
||||
Entry(ThreadLocal<?> k, Object v) {
|
||||
super(k);
|
||||
value = v;
|
||||
}
|
||||
`StampedLock` 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是`StampedLock`不可重入的原因。
|
||||
|
||||
```java
|
||||
// 写锁
|
||||
public long writeLock() {
|
||||
long s, next; // bypass acquireWrite in fully unlocked case only
|
||||
return ((((s = state) & ABITS) == 0L &&
|
||||
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
|
||||
next : acquireWrite(false, 0L));
|
||||
}
|
||||
// 读锁
|
||||
public long readLock() {
|
||||
long s = state, next; // bypass acquireRead on common uncontended case
|
||||
return ((whead == wtail && (s & ABITS) < RFULL &&
|
||||
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
|
||||
next : acquireRead(false, 0L));
|
||||
}
|
||||
// 乐观读
|
||||
public long tryOptimisticRead() {
|
||||
long s;
|
||||
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
|
||||
}
|
||||
```
|
||||
|
||||
**弱引用介绍:**
|
||||
### StampedLock 的性能为什么更好?
|
||||
|
||||
> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
|
||||
>
|
||||
> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
|
||||
相比于传统读写锁多出来的乐观读是`StampedLock`比 `ReadWriteLock` 性能更好的关键原因。`StampedLock` 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。
|
||||
|
||||
### StampedLock 适合什么场景?
|
||||
|
||||
和 `ReentrantReadWriteLock` 一样,`StampedLock` 同样适合读多写少的业务场景,可以作为 `ReentrantReadWriteLock`的替代品,性能更好。
|
||||
|
||||
不过,需要注意的是`StampedLock`不可重入,不支持条件变量 `Conditon`,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 `ReentrantLock` 的一些高级性能,就不太建议使用 `StampedLock` 了。
|
||||
|
||||
另外,`StampedLock` 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用`StampedLock` 之前,看看 [StampedLock 官方文档中的案例](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html)。
|
||||
|
||||
### StampedLock 的底层原理了解吗?
|
||||
|
||||
`StampedLock` 不是直接实现 `Lock`或 `ReadWriteLock`接口,而是基于 **CLH 锁** 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。`StampedLock` 通过 CLH 队列进行线程的管理,通过同步状态值 `state` 来表示锁的状态和类型。
|
||||
|
||||
`StampedLock` 的原理和 AQS 原理比较类似,这里就不详细介绍了,感兴趣的可以看看下面这两篇文章:
|
||||
|
||||
- [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html)
|
||||
- [StampedLock 底层原理分析](https://segmentfault.com/a/1190000015808032)
|
||||
|
||||
如果你只是准备面试的话,建议多花点经历搞懂 AQS 原理即可,StampedLock 底层原理在面试中遇到的概率非常小。
|
||||
|
||||
## Atomic 原子类
|
||||
|
||||
@ -656,5 +668,8 @@ Atomic 原子类部分的内容我单独写了一篇文章来总结: [Atomic
|
||||
- 《深入理解 Java 虚拟机》
|
||||
- 《实战 Java 高并发程序设计》
|
||||
- Guide to the Volatile Keyword in Java - Baeldung:https://www.baeldung.com/java-volatile
|
||||
- 不可不说的 Java“锁”事 - 美团技术团队:https://tech.meituan.com/2018/11/15/java-lock.html
|
||||
- 在 ReadWriteLock 类中读锁为什么不能升级为写锁?:https://cloud.tencent.com/developer/article/1176230
|
||||
- 高性能解决线程饥饿的利器 StampedLock:https://mp.weixin.qq.com/s/2Acujjr4BHIhlFsCLGwYSg
|
||||
- 理解 Java 中的 ThreadLocal - 技术小黑屋:https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/
|
||||
- ThreadLocal (Java Platform SE 8 ) - Oracle Help Center:https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html
|
||||
|
@ -12,6 +12,177 @@ head:
|
||||
content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助!
|
||||
---
|
||||
|
||||
## ThreadLocal
|
||||
|
||||
### ThreadLocal 有什么用?
|
||||
|
||||
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?**
|
||||
|
||||
JDK 中自带的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
|
||||
|
||||
如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
|
||||
|
||||
再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
|
||||
|
||||
### 如何使用 ThreadLocal?
|
||||
|
||||
相信看了上面的解释,大家已经搞懂 `ThreadLocal` 类是个什么东西了。下面简单演示一下如何在项目中实际使用 `ThreadLocal` 。
|
||||
|
||||
```java
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Random;
|
||||
|
||||
public class ThreadLocalExample implements Runnable{
|
||||
|
||||
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
|
||||
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
ThreadLocalExample obj = new ThreadLocalExample();
|
||||
for(int i=0 ; i<10; i++){
|
||||
Thread t = new Thread(obj, ""+i);
|
||||
Thread.sleep(new Random().nextInt(1000));
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
|
||||
try {
|
||||
Thread.sleep(new Random().nextInt(1000));
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//formatter pattern is changed here by thread, but it won't reflect to other threads
|
||||
formatter.set(new SimpleDateFormat());
|
||||
|
||||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
输出结果 :
|
||||
|
||||
```
|
||||
Thread Name= 0 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 0 formatter = yy-M-d ah:mm
|
||||
Thread Name= 1 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 2 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 1 formatter = yy-M-d ah:mm
|
||||
Thread Name= 3 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 2 formatter = yy-M-d ah:mm
|
||||
Thread Name= 4 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 3 formatter = yy-M-d ah:mm
|
||||
Thread Name= 4 formatter = yy-M-d ah:mm
|
||||
Thread Name= 5 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 5 formatter = yy-M-d ah:mm
|
||||
Thread Name= 6 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 6 formatter = yy-M-d ah:mm
|
||||
Thread Name= 7 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 7 formatter = yy-M-d ah:mm
|
||||
Thread Name= 8 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 9 default Formatter = yyyyMMdd HHmm
|
||||
Thread Name= 8 formatter = yy-M-d ah:mm
|
||||
Thread Name= 9 formatter = yy-M-d ah:mm
|
||||
```
|
||||
|
||||
从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。
|
||||
|
||||
上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
|
||||
|
||||
```java
|
||||
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue(){
|
||||
return new SimpleDateFormat("yyyyMMdd HHmm");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### ThreadLocal 原理了解吗?
|
||||
|
||||
从 `Thread`类源代码入手。
|
||||
|
||||
```java
|
||||
public class Thread implements Runnable {
|
||||
//......
|
||||
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
|
||||
ThreadLocal.ThreadLocalMap threadLocals = null;
|
||||
|
||||
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
|
||||
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
|
||||
//......
|
||||
}
|
||||
```
|
||||
|
||||
从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。
|
||||
|
||||
`ThreadLocal`类的`set()`方法
|
||||
|
||||
```java
|
||||
public void set(T value) {
|
||||
//获取当前请求的线程
|
||||
Thread t = Thread.currentThread();
|
||||
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null)
|
||||
// 将需要存储的值放入到这个哈希表中
|
||||
map.set(this, value);
|
||||
else
|
||||
createMap(t, value);
|
||||
}
|
||||
ThreadLocalMap getMap(Thread t) {
|
||||
return t.threadLocals;
|
||||
}
|
||||
```
|
||||
|
||||
通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
|
||||
|
||||
**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。**
|
||||
|
||||
```java
|
||||
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
|
||||
//......
|
||||
}
|
||||
```
|
||||
|
||||
比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话, `Thread`内部都是使用仅有的那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。
|
||||
|
||||
`ThreadLocal` 数据结构如下图所示:
|
||||
|
||||

|
||||
|
||||
`ThreadLocalMap`是`ThreadLocal`的静态内部类。
|
||||
|
||||

|
||||
|
||||
### ThreadLocal 内存泄露问题是怎么导致的?
|
||||
|
||||
`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
|
||||
|
||||
这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
|
||||
|
||||
```java
|
||||
static class Entry extends WeakReference<ThreadLocal<?>> {
|
||||
/** The value associated with this ThreadLocal. */
|
||||
Object value;
|
||||
|
||||
Entry(ThreadLocal<?> k, Object v) {
|
||||
super(k);
|
||||
value = v;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**弱引用介绍:**
|
||||
|
||||
> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
|
||||
>
|
||||
> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
|
||||
|
||||
## 线程池
|
||||
|
||||
### 什么是线程池?
|
||||
@ -32,20 +203,22 @@ head:
|
||||
|
||||
### 如何创建线程池?
|
||||
|
||||
**方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)**
|
||||
**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。**
|
||||
|
||||

|
||||
|
||||
**方式二:通过 `Executor` 框架的工具类 `Executors` 来实现**
|
||||
我们可以创建三种类型的 `ThreadPoolExecutor`:
|
||||
**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。**
|
||||
|
||||
我们可以创建多种类型的 `ThreadPoolExecutor`:
|
||||
|
||||
- **`FixedThreadPool`** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
|
||||
- **`SingleThreadExecutor`:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
||||
- **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
||||
- **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
|
||||
- **`ScheduledThreadPool`** :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
|
||||
|
||||
对应 Executors 工具类中的方法如图所示:
|
||||
对应 `Executors` 工具类中的方法如图所示:
|
||||
|
||||

|
||||

|
||||
|
||||
### 为什么不推荐使用内置线程池?
|
||||
|
||||
|
@ -47,7 +47,7 @@ tag:
|
||||
|
||||
如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **`ExecutorService`** 接口。
|
||||
|
||||

|
||||

|
||||
|
||||
这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。
|
||||
|
||||
@ -154,20 +154,24 @@ Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecu
|
||||
|
||||
### 线程池创建两种方式
|
||||
|
||||
**方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)**
|
||||
**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。**
|
||||
|
||||

|
||||
|
||||
**方式二:通过 `Executor` 框架的工具类 `Executors` 来实现**
|
||||
我们可以创建三种类型的 `ThreadPoolExecutor`:
|
||||
**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。**
|
||||
|
||||
我们可以创建多种类型的 `ThreadPoolExecutor`:
|
||||
|
||||
- **`FixedThreadPool`** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
|
||||
- **`SingleThreadExecutor`:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
||||
- **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
||||
- **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
|
||||
- **`ScheduledThreadPool`** :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
|
||||
|
||||
对应 `Executors` 工具类中的方法如图所示:
|
||||
|
||||

|
||||
|
||||
对应 Executors 工具类中的方法如图所示:
|
||||
|
||||

|
||||
|
||||
《阿里巴巴 Java 开发手册》强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
|
||||
|
||||
@ -392,6 +396,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020
|
||||
addWorker(null, false);
|
||||
}
|
||||
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
|
||||
// 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize
|
||||
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
|
||||
else if (!addWorker(command, false))
|
||||
reject(command);
|
||||
@ -409,7 +414,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020
|
||||
|
||||

|
||||
|
||||
`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。
|
||||
在 `execute` 方法中,多次调用 `addWorker` 方法。`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。
|
||||
|
||||
```java
|
||||
// 全局锁,并发操作必备
|
||||
|
@ -9,9 +9,9 @@ tag:
|
||||
>
|
||||
> 作者:美团技术团队
|
||||
|
||||
## 前言
|
||||
Java 中的大部分同步类(Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
|
||||
|
||||
Java 中的大部分同步类(Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层,并通过 ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点,同时采取问答的模式来帮助大家理解 AQS。由于篇幅原因,本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue,不讲述包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。
|
||||
本文会从应用层逐渐深入到原理层,并通过 ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点,同时采取问答的模式来帮助大家理解 AQS。由于篇幅原因,本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue,不讲述包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。
|
||||
|
||||
## 1 ReentrantLock
|
||||
|
||||
@ -19,7 +19,7 @@ Java 中的大部分同步类(Semaphore、ReentrantLock 等)都是基于 Abs
|
||||
|
||||
ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点):
|
||||
|
||||

|
||||

|
||||
|
||||
下面通过伪代码,进行更加直观的比较:
|
||||
|
||||
@ -200,7 +200,7 @@ private volatile int state;
|
||||
|
||||
对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是 AQS 架构图中的第一层:API 层。
|
||||
|
||||
## 2.2 AQS 重要方法与 ReentrantLock 的关联
|
||||
### 2.2 AQS 重要方法与 ReentrantLock 的关联
|
||||
|
||||
从架构图中可以得知,AQS 提供了大量用于自定义同步器实现的 Protected 方法。自定义同步器实现的相关方法也只是为了通过修改 State 字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock 需要实现的方法如下,并不是全部):
|
||||
|
||||
@ -266,7 +266,7 @@ private volatile int state;
|
||||
|
||||

|
||||
|
||||
## 2.3 通过 ReentrantLock 理解 AQS
|
||||
## 3 通过 ReentrantLock 理解 AQS
|
||||
|
||||
ReentrantLock 中公平锁和非公平锁在底层是相同的,这里以非公平锁为例进行分析。
|
||||
|
||||
@ -310,13 +310,13 @@ protected boolean tryAcquire(int arg) {
|
||||
|
||||
可以看出,这里只是 AQS 的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以 ReentrantLock 为例)。如果该方法返回了 True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。
|
||||
|
||||
### 2.3.1 线程加入等待队列
|
||||
### 3.1 线程加入等待队列
|
||||
|
||||
#### 2.3.1.1 加入队列的时机
|
||||
#### 3.1.1 加入队列的时机
|
||||
|
||||
当执行 Acquire(1)时,会通过 tryAcquire 获取锁。在这种情况下,如果获取锁失败,就会调用 addWaiter 加入到等待队列中去。
|
||||
|
||||
#### 2.3.1.2 如何加入队列
|
||||
#### 3.1.2 如何加入队列
|
||||
|
||||
获取锁失败后,会执行 addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下:
|
||||
|
||||
@ -393,12 +393,13 @@ private Node enq(final Node node) {
|
||||
|
||||
总结一下,线程获取锁的时候,过程大体如下:
|
||||
|
||||
1. 当没有线程获取到锁时,线程 1 获取锁成功。
|
||||
2. 线程 2 申请锁,但是锁被线程 1 占有。
|
||||
1、当没有线程获取到锁时,线程 1 获取锁成功。
|
||||
|
||||
2、线程 2 申请锁,但是锁被线程 1 占有。
|
||||
|
||||

|
||||
|
||||
1. 如果再有线程要获取锁,依次在队列中往后排队即可。
|
||||
3、如果再有线程要获取锁,依次在队列中往后排队即可。
|
||||
|
||||
回到上边的代码,hasQueuedPredecessors 是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回 False,说明当前线程可以争取共享资源;如果返回 True,说明队列中存在有效节点,当前线程必须加入到等待队列中。
|
||||
|
||||
@ -437,7 +438,7 @@ if (t == null) { // Must initialize
|
||||
|
||||
节点入队不是原子操作,所以会出现短暂的 head != tail,此时 Tail 指向最后一个节点,而且 Tail 指向 Head。如果 Head 没有指向 Tail(可见 5、6、7 行),这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。
|
||||
|
||||
#### 2.3.1.3 等待队列中线程出队列时机
|
||||
#### 3.1.3 等待队列中线程出队列时机
|
||||
|
||||
回到最初的源码:
|
||||
|
||||
@ -547,7 +548,7 @@ private final boolean parkAndCheckInterrupt() {
|
||||
- shouldParkAfterFailedAcquire 中取消节点是怎么生成的呢?什么时候会把一个节点的 waitStatus 设置为-1?
|
||||
- 是在什么时间释放节点通知到被挂起的线程呢?
|
||||
|
||||
### 2.3.2 CANCELLED 状态节点生成
|
||||
### 3.2 CANCELLED 状态节点生成
|
||||
|
||||
acquireQueued 方法中的 Finally 代码:
|
||||
|
||||
@ -649,7 +650,7 @@ private void cancelAcquire(Node node) {
|
||||
> } while (pred.waitStatus > 0);
|
||||
> ```
|
||||
|
||||
### 2.3.3 如何解锁
|
||||
### 3.3 如何解锁
|
||||
|
||||
我们已经剖析了加锁过程中的基本流程,接下来再对解锁的基本流程进行分析。由于 ReentrantLock 在解锁的时候,并不区分公平锁和非公平锁,所以我们直接看解锁的源码:
|
||||
|
||||
@ -780,7 +781,7 @@ private Node addWaiter(Node mode) {
|
||||
|
||||
综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和 CANCELLED 节点产生过程中断开 Next 指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。继续执行 acquireQueued 方法以后,中断如何处理?
|
||||
|
||||
### 2.3.4 中断恢复后的执行流程
|
||||
### 3.4 中断恢复后的执行流程
|
||||
|
||||
唤醒后,会执行 return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。
|
||||
|
||||
@ -837,7 +838,7 @@ static void selfInterrupt() {
|
||||
|
||||
这里的处理方式主要是运用线程池中基本运作单元 Worder 中的 runWorker,通过 Thread.interrupted()进行额外的判断处理,感兴趣的同学可以看下 ThreadPoolExecutor 源码。
|
||||
|
||||
### 2.3.5 小结
|
||||
### 3.5 小结
|
||||
|
||||
我们在 1.3 小节中提出了一些问题,现在来回答一下。
|
||||
|
||||
@ -861,9 +862,9 @@ static void selfInterrupt() {
|
||||
>
|
||||
> A:AQS 的 Acquire 会调用 tryAcquire 方法,tryAcquire 由各个自定义同步器实现,通过 tryAcquire 完成加锁过程。
|
||||
|
||||
## 3 AQS 应用
|
||||
## 4 AQS 应用
|
||||
|
||||
### 3.1 ReentrantLock 的可重入应用
|
||||
### 4.1 ReentrantLock 的可重入应用
|
||||
|
||||
ReentrantLock 的可重入性是 AQS 很好的应用之一,在了解完上述知识点以后,我们很容易得知 ReentrantLock 实现可重入的方法。在 ReentrantLock 里面,不管是公平锁还是非公平锁,都有一段逻辑。
|
||||
|
||||
@ -921,7 +922,7 @@ private volatile int state;
|
||||
2. 当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁是,就会多次+1,这里就是可重入的概念。
|
||||
3. 解锁也是对这个字段-1,一直到 0,此线程对锁释放。
|
||||
|
||||
### 3.2 JUC 中的应用场景
|
||||
### 4.2 JUC 中的应用场景
|
||||
|
||||
除了上边 ReentrantLock 的可重入性的应用,AQS 作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了 JUC 中的几种同步工具,大体介绍一下 AQS 的应用场景:
|
||||
|
||||
@ -933,7 +934,7 @@ private volatile int state;
|
||||
| ReentrantReadWriteLock | 使用 AQS 同步状态中的 16 位保存写锁持有的次数,剩下的 16 位用于保存读锁的持有次数。 |
|
||||
| ThreadPoolExecutor | Worker 利用 AQS 同步状态实现对独占线程变量的设置(tryAcquire 和 tryRelease)。 |
|
||||
|
||||
### 3.3 自定义同步工具
|
||||
### 4.3 自定义同步工具
|
||||
|
||||
了解 AQS 基本原理以后,按照上面所说的 AQS 知识点,自己实现一个同步工具。
|
||||
|
||||
@ -1009,7 +1010,7 @@ public class LeeMain {
|
||||
|
||||
上述代码每次运行结果都会是 20000。通过简单的几行代码就能实现同步功能,这就是 AQS 的强大之处。
|
||||
|
||||
## 总结
|
||||
## 5 总结
|
||||
|
||||
我们日常开发中使用并发的场景太多,但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因,本文仅介绍了可重入锁 ReentrantLock 的原理和 AQS 原理,希望能够成为大家了解 AQS 和 ReentrantLock 等同步器的“敲门砖”。
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user