1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-25 02:27:10 +08:00

[docs fix]乐观锁和悲观锁详解中删除适用场景的介绍

This commit is contained in:
Guide 2023-03-21 22:02:04 +08:00
parent 9e2a6e8808
commit 1a64bb5225
7 changed files with 78 additions and 27 deletions

View File

@ -99,7 +99,7 @@
- [Java 并发容器总结](./docs/java/concurrent/java-concurrent-collections.md)
- [Atomic 原子类总结](./docs/java/concurrent/atomic-classes.md)
- [AQS 详解](./docs/java/concurrent/aqs.md)
- [CompletableFuture入门](./docs/java/concurrent/completablefuture-intro.md)
- [CompletableFuture详解](./docs/java/concurrent/completablefuture-intro.md)
### JVM (必看 :+1:)

View File

@ -257,7 +257,7 @@ public static int[] shellSort(int[] arr) {
### 算法分析
- **稳定性**:稳定
- **稳定性**稳定
- **时间复杂度** 最佳O(nlogn) 最差O(n2) 平均O(nlogn)
- **空间复杂度** `O(1)`

View File

@ -1,5 +1,5 @@
---
title: MySQL 查询缓存详解
title: MySQL查询缓存详解
category: 数据库
tag:
- MySQL

View File

@ -101,7 +101,7 @@ title: JavaGuideJava学习&&面试指南)
- [Java 并发容器总结](./java/concurrent/java-concurrent-collections.md)
- [Atomic 原子类总结](./java/concurrent/atomic-classes.md)
- [AQS 详解](./java/concurrent/aqs.md)
- [CompletableFuture入门](./java/concurrent/completablefuture-intro.md)
- [CompletableFuture详解](./java/concurrent/completablefuture-intro.md)
### JVM (必看 :+1:)

View File

@ -1,5 +1,5 @@
---
title: CompletableFuture入门
title: CompletableFuture详解
category: Java
tag:
- Java并发

View File

@ -174,23 +174,46 @@ public void increase() {
## 乐观锁和悲观锁
### 什么是悲观锁?使用场景是什么?
### 什么是悲观锁?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
像 Java 中`synchronized``ReentrantLock`等独占锁就是悲观锁思想的实现。
**悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
```java
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
### 什么是乐观锁?使用场景是什么?
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
```
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
### 什么是乐观锁?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger``LongAdder`就是使用了乐观锁的一种实现方式 **CAS** 实现的。
**乐观锁通常多用于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
```java
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间(空间换时间)
LongAdder sum = new LongAdder();
sum.increment();
```
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。
### 如何实现乐观锁?

View File

@ -5,27 +5,50 @@ tag:
- Java并发
---
如果将悲观锁和乐观锁对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
如果将悲观锁Pessimistic Lock和乐观锁PessimisticLock 或 OptimisticLock对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大!
## 什么是悲观锁?使用场景是什么?
## 什么是悲观锁?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
像 Java 中`synchronized``ReentrantLock`等独占锁就是悲观锁思想的实现。
**悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能。**
```java
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
## 什么是乐观锁?使用场景是什么?
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
```
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
## 什么是乐观锁?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。
在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger``LongAdder`就是使用了乐观锁的一种实现方式 **CAS** 实现的。
**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。**
```java
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间(空间换时间)
LongAdder sum = new LongAdder();
sum.increment();
```
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。
## 如何实现乐观锁?
@ -58,16 +81,16 @@ CAS 涉及到三个操作数:
- **E** :预期值(Expected)
- **N** :拟写入的新值(New)
当且仅当 V 的值等于 E 时CAS 通过原子方式用新值 N 来更新 V 的值。如果不等说明已经有其它线程更新了V则当前线程放弃更新。
当且仅当 V 的值等于 E 时CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V则当前线程放弃更新。
**举一个简单的例子** :线程 A 要修改变量 i 的值为 6i 原值为 1V = 1E=1N=6假设不存在 ABA 问题)。
1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
2. i 与1 进行比较如果不相等则说明被其他线程修改当前线程放弃更新CAS 操作失败。
1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
2. i 与 1 进行比较如果不相等则说明被其他线程修改当前线程放弃更新CAS 操作失败。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
Java 语言并没有直接实现 CASCAS 相关的实现是通过 C++ 内联汇编的形式实现的JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。
Java 语言并没有直接实现 CASCAS 相关的实现是通过 C++ 内联汇编的形式实现的JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。
`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject``compareAndSwapInt``compareAndSwapLong`方法来实现的对`Object``int``long`类型的 CAS 操作
@ -97,7 +120,7 @@ ABA 问题是乐观锁最常见的问题。
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
```java
public boolean compareAndSet(V expectedReference,
@ -126,3 +149,8 @@ CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循
### 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
## 参考
- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现https://zhuanlan.zhihu.com/p/71156910
- 一文彻底搞懂CAS实现原理 & 深入到CPU指令https://zhuanlan.zhihu.com/p/94976168