|
5 | 5 | - Java并发
|
6 | 6 | ---
|
7 | 7 |
|
8 |
| -如果将悲观锁和乐观锁对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 |
| 8 | +如果将悲观锁(Pessimistic Lock)和乐观锁(PessimisticLock 或 OptimisticLock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 |
9 | 9 |
|
10 | 10 | 在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大!
|
11 | 11 |
|
12 |
| -## 什么是悲观锁?使用场景是什么? |
| 12 | +## 什么是悲观锁? |
13 | 13 |
|
14 |
| -悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。 |
15 |
| - |
16 |
| -也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 |
| 14 | +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 |
17 | 15 |
|
18 | 16 | 像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。
|
19 | 17 |
|
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 | +高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。 |
21 | 35 |
|
22 |
| -## 什么是乐观锁?使用场景是什么? |
| 36 | +## 什么是乐观锁? |
23 | 37 |
|
24 | 38 | 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
|
25 | 39 |
|
26 |
| -在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。 |
| 40 | +在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 |
27 | 41 |
|
28 |
| -**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。** |
| 42 | +```java |
| 43 | +// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 |
| 44 | +// 代价就是会消耗更多的内存空间(空间换时间) |
| 45 | +LongAdder sum = new LongAdder(); |
| 46 | +sum.increment(); |
| 47 | +``` |
| 48 | + |
| 49 | +高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 |
| 50 | + |
| 51 | +不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。 |
29 | 52 |
|
30 | 53 | ## 如何实现乐观锁?
|
31 | 54 |
|
@@ -58,16 +81,16 @@ CAS 涉及到三个操作数:
|
58 | 81 | - **E** :预期值(Expected)
|
59 | 82 | - **N** :拟写入的新值(New)
|
60 | 83 |
|
61 |
| -当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。 |
| 84 | +当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。 |
62 | 85 |
|
63 | 86 | **举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
|
64 | 87 |
|
65 |
| -1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 |
66 |
| -2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 |
| 88 | +1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 |
| 89 | +2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 |
67 | 90 |
|
68 | 91 | 当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
|
69 | 92 |
|
70 |
| -Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。 |
| 93 | +Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。 |
71 | 94 |
|
72 | 95 | `sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作
|
73 | 96 |
|
@@ -97,7 +120,7 @@ ABA 问题是乐观锁最常见的问题。
|
97 | 120 |
|
98 | 121 | 如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
|
99 | 122 |
|
100 |
| -ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 |
| 123 | +ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 |
101 | 124 |
|
102 | 125 | ```java
|
103 | 126 | public boolean compareAndSet(V expectedReference,
|
@@ -126,3 +149,8 @@ CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循
|
126 | 149 | ### 只能保证一个共享变量的原子操作
|
127 | 150 |
|
128 | 151 | 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