mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-25 02:27:10 +08:00
[docs fix]乐观锁和悲观锁详解中删除适用场景的介绍
This commit is contained in:
parent
9e2a6e8808
commit
1a64bb5225
@ -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:)
|
||||
|
||||
|
@ -257,7 +257,7 @@ public static int[] shellSort(int[] arr) {
|
||||
|
||||
### 算法分析
|
||||
|
||||
- **稳定性**:稳定
|
||||
- **稳定性**:不稳定
|
||||
- **时间复杂度** :最佳:O(nlogn), 最差:O(n2) 平均:O(nlogn)
|
||||
- **空间复杂度** :`O(1)`
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: MySQL 查询缓存详解
|
||||
title: MySQL查询缓存详解
|
||||
category: 数据库
|
||||
tag:
|
||||
- MySQL
|
||||
|
@ -101,7 +101,7 @@ title: JavaGuide(Java学习&&面试指南)
|
||||
- [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:)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: CompletableFuture入门
|
||||
title: CompletableFuture详解
|
||||
category: Java
|
||||
tag:
|
||||
- Java并发
|
||||
|
@ -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`以空间换时间的方式就解决了这个问题。
|
||||
|
||||
### 如何实现乐观锁?
|
||||
|
||||
|
@ -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 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
|
||||
|
||||
1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
|
||||
2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
|
||||
1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
|
||||
2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
|
||||
|
||||
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
|
||||
|
||||
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。
|
||||
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user