Skip to content

Commit fe697ad

Browse files
committed
[docs update&add]完善部分内容描述&新增一篇cas详解
1 parent 97dd88a commit fe697ad

File tree

9 files changed

+301
-84
lines changed

9 files changed

+301
-84
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888

8989
**重要知识点详解**
9090

91+
- [乐观锁和悲观锁详解](./docs/java/concurrent/jmm.md)
92+
- [CAS 详解](./docs/java/concurrent/cas.md)
9193
- [JMM(Java 内存模型)详解](./docs/java/concurrent/jmm.md)
9294
- **线程池**[Java 线程池详解](./docs/java/concurrent/java-thread-pool-summary.md)[Java 线程池最佳实践](./docs/java/concurrent/java-thread-pool-best-practices.md)
9395
- [ThreadLocal 详解](./docs/java/concurrent/threadlocal.md)

Diff for: docs/.vuepress/sidebar/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export default sidebar({
112112
collapsible: true,
113113
children: [
114114
"optimistic-lock-and-pessimistic-lock",
115+
"cas",
115116
"jmm",
116117
"java-thread-pool-summary",
117118
"java-thread-pool-best-practices",

Diff for: docs/database/mysql/mysql-index.md

+8
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,14 @@ EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk'
385385
SELECT * FROM student WHERE class = 'lIrm08RYVk';
386386
```
387387

388+
再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1`会走索引么?`c=1` 呢?`b=1 AND c=1`呢?
389+
390+
先不要往下看答案,给自己 3 分钟时间想一想。
391+
392+
1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。
393+
2. 查询 `c=1` :由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
394+
3. 查询`b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
395+
388396
MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。
389397

390398
## 索引下推

Diff for: docs/database/sql/sql-syntax-summary.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ WHERE username = 'root';
148148
### 删除数据
149149

150150
- `DELETE` 语句用于删除表中的记录。
151-
- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。
151+
- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。说明:`TRUNCATE` 语句不属于 DML 语法而是 DDL 语法。
152152

153153
**删除表中的指定数据**
154154

Diff for: docs/home.md

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ title: JavaGuide(Java学习&面试指南)
7272

7373
**重要知识点详解**
7474

75+
- [乐观锁和悲观锁详解](./java/concurrent/jmm.md)
76+
- [CAS 详解](./java/concurrent/cas.md)
7577
- [JMM(Java 内存模型)详解](./java/concurrent/jmm.md)
7678
- **线程池**[Java 线程池详解](./java/concurrent/java-thread-pool-summary.md)[Java 线程池最佳实践](./java/concurrent/java-thread-pool-best-practices.md)
7779
- [ThreadLocal 详解](./java/concurrent/threadlocal.md)

Diff for: docs/java/basis/java-basic-questions-02.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public class Student {
216216

217217
在 Java 8 及以上版本中,接口引入了新的方法类型:`default` 方法、`static` 方法和 `private` 方法。这些方法让接口的使用更加灵活。
218218

219-
Java 8 引入的`default` 方法用于提供接口方法的默认实现,可以在实现类中被覆盖。
219+
Java 8 引入的`default` 方法用于提供接口方法的默认实现,可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能,从而增强接口的扩展性和向后兼容性。
220220

221221
```java
222222
public interface MyInterface {
@@ -226,7 +226,7 @@ public interface MyInterface {
226226
}
227227
```
228228

229-
Java 8 引入的`static` 方法无法在实现类中被覆盖,只能通过接口名直接调用( `MyInterface.staticMethod()`),类似于类中的静态方法。
229+
Java 8 引入的`static` 方法无法在实现类中被覆盖,只能通过接口名直接调用( `MyInterface.staticMethod()`),类似于类中的静态方法。`static` 方法通常用于定义一些通用的、与接口相关的工具方法,一般很少用。
230230

231231
```java
232232
public interface MyInterface {
@@ -255,7 +255,7 @@ public interface MyInterface {
255255
System.out.println("This is a private method used internally.");
256256
}
257257

258-
// 私有方法,只能被 default 方法调用。
258+
// 实例私有方法,只能被 default 方法调用。
259259
private void instanceCommonMethod() {
260260
System.out.println("This is a private instance method used internally.");
261261
}

Diff for: docs/java/concurrent/cas.md

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
title: CAS 详解
3+
category: Java
4+
tag:
5+
- Java并发
6+
---
7+
8+
乐观锁和悲观锁的介绍以及乐观锁常见实现方式可以阅读笔者写的这篇文章:[乐观锁和悲观锁详解](https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html)
9+
10+
这篇文章主要介绍 :Java 中 CAS 的实现以及 CAS 存在的一些问题。
11+
12+
## Java 中 CAS 是如何实现的?
13+
14+
在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是`Unsafe`
15+
16+
`Unsafe`类位于`sun.misc`包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。关于 `Unsafe`类的详细介绍,可以阅读这篇文章:📌[Java 魔法类 Unsafe 详解](https://javaguide.cn/java/basis/unsafe.html)
17+
18+
`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject``compareAndSwapInt``compareAndSwapLong`方法来实现的对`Object``int``long`类型的 CAS 操作:
19+
20+
```java
21+
/**
22+
* 以原子方式更新对象字段的值。
23+
*
24+
* @param o 要操作的对象
25+
* @param offset 对象字段的内存偏移量
26+
* @param expected 期望的旧值
27+
* @param x 要设置的新值
28+
* @return 如果值被成功更新,则返回 true;否则返回 false
29+
*/
30+
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
31+
32+
/**
33+
* 以原子方式更新 int 类型的对象字段的值。
34+
*/
35+
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
36+
37+
/**
38+
* 以原子方式更新 long 类型的对象字段的值。
39+
*/
40+
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
41+
```
42+
43+
`Unsafe`类中的 CAS 方法是`native`方法。`native`关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS,而是通过 C++ 内联汇编的形式实现的(通过 JNI 调用)。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。
44+
45+
`java.util.concurrent.atomic` 包提供了一些用于原子操作的类。这些类利用底层的原子指令,确保在多线程环境下的操作是线程安全的。
46+
47+
![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png)
48+
49+
关于这些 Atomic 原子类的介绍和使用,可以阅读这篇文章:[Atomic 原子类总结](https://javaguide.cn/java/concurrent/atomic-classes.html)
50+
51+
`AtomicInteger`是 Java 的原子类之一,主要用于对 `int` 类型的变量进行原子操作,它利用`Unsafe`类提供的低级别原子操作方法实现无锁的线程安全性。
52+
53+
下面,我们通过解读`AtomicInteger`的核心源码(JDK1.8),来说明 Java 如何使用`Unsafe`类的方法来实现原子操作。
54+
55+
`AtomicInteger`核心源码如下:
56+
57+
```java
58+
// 获取 Unsafe 实例
59+
private static final Unsafe unsafe = Unsafe.getUnsafe();
60+
private static final long valueOffset;
61+
62+
static {
63+
try {
64+
// 获取“value”字段在AtomicInteger类中的内存偏移量
65+
valueOffset = unsafe.objectFieldOffset
66+
(AtomicInteger.class.getDeclaredField("value"));
67+
} catch (Exception ex) { throw new Error(ex); }
68+
}
69+
// 确保“value”字段的可见性
70+
private volatile int value;
71+
72+
// 如果当前值等于预期值,则原子地将值设置为newValue
73+
// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作
74+
public final boolean compareAndSet(int expect, int update) {
75+
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
76+
}
77+
78+
// 原子地将当前值加 delta 并返回旧值
79+
public final int getAndAdd(int delta) {
80+
return unsafe.getAndAddInt(this, valueOffset, delta);
81+
}
82+
83+
// 原子地将当前值加 1 并返回加之前的值(旧值)
84+
// 使用 Unsafe#getAndAddInt 方法进行CAS操作。
85+
public final int getAndIncrement() {
86+
return unsafe.getAndAddInt(this, valueOffset, 1);
87+
}
88+
89+
// 原子地将当前值减 1 并返回减之前的值(旧值)
90+
public final int getAndDecrement() {
91+
return unsafe.getAndAddInt(this, valueOffset, -1);
92+
}
93+
```
94+
95+
`Unsafe#getAndAddInt`源码:
96+
97+
```java
98+
// 原子地获取并增加整数值
99+
public final int getAndAddInt(Object o, long offset, int delta) {
100+
int v;
101+
do {
102+
// 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值
103+
v = getIntVolatile(o, offset);
104+
} while (!compareAndSwapInt(o, offset, v, v + delta));
105+
// 返回旧值
106+
return v;
107+
}
108+
```
109+
110+
可以看到,`getAndAddInt` 使用了 `do-while` 循环:在`compareAndSwapInt`操作失败时,会不断重试直到成功。也就是说,`getAndAddInt`方法会通过 `compareAndSwapInt` 方法来尝试更新 `value` 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。
111+
112+
由于 CAS 操作可能会因为并发冲突而失败,因此通常会与`while`循环搭配使用,在失败后不断重试,直到操作成功。这就是 **自旋锁机制**
113+
114+
## CAS 算法存在哪些问题?
115+
116+
ABA 问题是 CAS 算法最常见的问题。
117+
118+
### ABA 问题
119+
120+
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。**
121+
122+
ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
123+
124+
```java
125+
public boolean compareAndSet(V expectedReference,
126+
V newReference,
127+
int expectedStamp,
128+
int newStamp) {
129+
Pair<V> current = pair;
130+
return
131+
expectedReference == current.reference &&
132+
expectedStamp == current.stamp &&
133+
((newReference == current.reference &&
134+
newStamp == current.stamp) ||
135+
casPair(current, Pair.of(newReference, newStamp)));
136+
}
137+
```
138+
139+
### 循环时间长开销大
140+
141+
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
142+
143+
如果 JVM 能够支持处理器提供的`pause`指令,那么自旋操作的效率将有所提升。`pause`指令有两个重要作用:
144+
145+
1. **延迟流水线执行指令**`pause`指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。
146+
2. **避免内存顺序冲突**:在退出循环时,`pause`指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。
147+
148+
### 只能保证一个共享变量的原子操作
149+
150+
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
151+
152+
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。
153+
154+
除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。
155+
156+
## 总结
157+
158+
在 Java 中,CAS 通过 `Unsafe` 类中的 `native` 方法实现,这些方法调用底层的硬件指令来完成原子操作。由于其实现依赖于 C++ 内联汇编和 JNI 调用,因此 CAS 的具体实现与操作系统以及 CPU 密切相关。
159+
160+
CAS 作为实现乐观锁的核心算法,虽然具有高效的无锁特性,但也需要注意 ABA 问题、循环时间长开销大等问题。

0 commit comments

Comments
 (0)