Skip to content

Commit 1a64bb5

Browse files
committed
[docs fix]乐观锁和悲观锁详解中删除适用场景的介绍
1 parent 9e2a6e8 commit 1a64bb5

7 files changed

+78
-27
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
- [Java 并发容器总结](./docs/java/concurrent/java-concurrent-collections.md)
100100
- [Atomic 原子类总结](./docs/java/concurrent/atomic-classes.md)
101101
- [AQS 详解](./docs/java/concurrent/aqs.md)
102-
- [CompletableFuture入门](./docs/java/concurrent/completablefuture-intro.md)
102+
- [CompletableFuture详解](./docs/java/concurrent/completablefuture-intro.md)
103103

104104
### JVM (必看 :+1:)
105105

docs/cs-basics/algorithms/10-classical-sorting-algorithms.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ public static int[] shellSort(int[] arr) {
257257

258258
### 算法分析
259259

260-
- **稳定性**稳定
260+
- **稳定性**不稳定
261261
- **时间复杂度** :最佳:O(nlogn), 最差:O(n2) 平均:O(nlogn)
262262
- **空间复杂度**`O(1)`
263263

docs/database/mysql/mysql-query-cache.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: MySQL 查询缓存详解
2+
title: MySQL查询缓存详解
33
category: 数据库
44
tag:
55
- MySQL

docs/home.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ title: JavaGuide(Java学习&&面试指南)
101101
- [Java 并发容器总结](./java/concurrent/java-concurrent-collections.md)
102102
- [Atomic 原子类总结](./java/concurrent/atomic-classes.md)
103103
- [AQS 详解](./java/concurrent/aqs.md)
104-
- [CompletableFuture入门](./java/concurrent/completablefuture-intro.md)
104+
- [CompletableFuture详解](./java/concurrent/completablefuture-intro.md)
105105

106106
### JVM (必看 :+1:)
107107

docs/java/concurrent/completablefuture-intro.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: CompletableFuture入门
2+
title: CompletableFuture详解
33
category: Java
44
tag:
55
- Java并发

docs/java/concurrent/java-concurrent-questions-02.md

+31-8
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,46 @@ public void increase() {
174174

175175
## 乐观锁和悲观锁
176176

177-
### 什么是悲观锁?使用场景是什么?
177+
### 什么是悲观锁?
178178

179-
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
180-
181-
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**
179+
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**
182180

183181
像 Java 中`synchronized``ReentrantLock`等独占锁就是悲观锁思想的实现。
184182

185-
**悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
183+
```java
184+
public void performSynchronisedTask() {
185+
synchronized (this) {
186+
// 需要同步的操作
187+
}
188+
}
189+
190+
private Lock lock = new ReentrantLock();
191+
lock.lock();
192+
try {
193+
// 需要同步的操作
194+
} finally {
195+
lock.unlock();
196+
}
197+
```
198+
199+
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
186200

187-
### 什么是乐观锁?使用场景是什么?
201+
### 什么是乐观锁?
188202

189203
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
190204

191-
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
205+
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger``LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。
206+
207+
```java
208+
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
209+
// 代价就是会消耗更多的内存空间(空间换时间)
210+
LongAdder sum = new LongAdder();
211+
sum.increment();
212+
```
213+
214+
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
192215

193-
**乐观锁通常多用于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
216+
不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。
194217

195218
### 如何实现乐观锁?
196219

docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md

+42-14
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,50 @@ tag:
55
- Java并发
66
---
77

8-
如果将悲观锁和乐观锁对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
8+
如果将悲观锁(Pessimistic Lock)和乐观锁(PessimisticLock 或 OptimisticLock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
99

1010
在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大!
1111

12-
## 什么是悲观锁?使用场景是什么?
12+
## 什么是悲观锁?
1313

14-
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
15-
16-
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**
14+
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**
1715

1816
像 Java 中`synchronized``ReentrantLock`等独占锁就是悲观锁思想的实现。
1917

20-
**悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
18+
```java
19+
public void performSynchronisedTask() {
20+
synchronized (this) {
21+
// 需要同步的操作
22+
}
23+
}
24+
25+
private Lock lock = new ReentrantLock();
26+
lock.lock();
27+
try {
28+
// 需要同步的操作
29+
} finally {
30+
lock.unlock();
31+
}
32+
```
33+
34+
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
2135

22-
## 什么是乐观锁?使用场景是什么?
36+
## 什么是乐观锁?
2337

2438
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
2539

26-
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
40+
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger``LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。
2741

28-
**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
42+
```java
43+
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
44+
// 代价就是会消耗更多的内存空间(空间换时间)
45+
LongAdder sum = new LongAdder();
46+
sum.increment();
47+
```
48+
49+
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
50+
51+
不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。
2952

3053
## 如何实现乐观锁?
3154

@@ -58,16 +81,16 @@ CAS 涉及到三个操作数:
5881
- **E** :预期值(Expected)
5982
- **N** :拟写入的新值(New)
6083

61-
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。
84+
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
6285

6386
**举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
6487

65-
1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
66-
2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
88+
1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
89+
2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
6790

6891
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
6992

70-
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系
93+
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系
7194

7295
`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject``compareAndSwapInt``compareAndSwapLong`方法来实现的对`Object``int``long`类型的 CAS 操作
7396

@@ -97,7 +120,7 @@ ABA 问题是乐观锁最常见的问题。
97120

98121
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
99122

100-
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
123+
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
101124

102125
```java
103126
public boolean compareAndSet(V expectedReference,
@@ -126,3 +149,8 @@ CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循
126149
### 只能保证一个共享变量的原子操作
127150

128151
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
152+
153+
## 参考
154+
155+
- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!:https://zhuanlan.zhihu.com/p/71156910
156+
- 一文彻底搞懂CAS实现原理 & 深入到CPU指令:https://zhuanlan.zhihu.com/p/94976168

0 commit comments

Comments
 (0)