mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-14 05:21:42 +08:00
[docs update]添加ReentrantLock相关内容
This commit is contained in:
parent
c706bebb0d
commit
acf10f6e03
@ -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,39 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
||||
- `volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。
|
||||
- `volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。
|
||||
|
||||
## ReentrantLock
|
||||
|
||||
### ReentrantLock 是什么?
|
||||
|
||||
`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,和 `synchronized` 关键字类似。不过,`ReentrantLock` 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
|
||||
|
||||
`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` 关键字锁都是可重入的。
|
||||
|
||||
#### synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
|
||||
|
||||
@ -467,11 +493,6 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
||||
|
||||
如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。
|
||||
|
||||
关于公平锁和非公平锁的补充:
|
||||
|
||||
> - **公平锁** : 锁被释放之后,先申请的线程/进程先得到锁。
|
||||
> - **非公平锁** :锁被释放之后,后申请的线程/进程可能会先获取到锁,是随机或者按照其他优先级排序的。
|
||||
|
||||
关于 `Condition`接口的补充:
|
||||
|
||||
> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。
|
||||
|
@ -392,6 +392,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 +410,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020
|
||||
|
||||

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

|
||||

|
||||
|
||||
下面通过伪代码,进行更加直观的比较:
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user