Files
OneMD/posts/blog/编程技术/java/JUC/JUC笔记.md
T
2026-06-19 14:45:07 +08:00

647 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: JUC笔记
date: 2024-08-25
tags: [JUC]
---
- JUCjava.util.concurrent 并发编程
![image 30.png](JUC笔记/image30.png)
## 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类(juc
这里说它们是线程安全的是指,**多个线程调用它们同一个实例**的某个方法时,是线程安全的。但是自己手动组合方法是线程不安全的
> [!important] String、Integer是不可变线程,因为值是不可修改
### 效率问题
1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用
cpu ,不至于一个线程总占用 cpu,别的线程没法干活
2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任
务都能拆分(参考后文的【阿姆达尔定律】)
也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一
直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
> 注意
> 需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
### 原子性
- 原子操作可以保证原子性,加锁本质是保证了多线程修改操作为原子操作
- 原子操作:一个操作或者多个操作组合在一起,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。特点:不可打断、一致性
- 两个线程对一个变量进行修改,产生线程问题就是因为修改不是原子操作
例如对于 i++、i-- 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
```Java
//i++操作
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
//i--操作
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
```
多线程,指令交错执行
```Java
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
```
## synchronized原理
### Monitor 概念
- Java对象头:内存结构
- 普通对象
```Java
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
```
- 数组对象
```Java
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
```
- 其中 Mark Word 结构为
```Java
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
```
```Java
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
```
- Monitor锁,记录了持有对象,阻塞线程,等待线程,Monitor是==操作系统提供的==,由c++实现的,每个锁对象关联一个Monitor锁(**锁对象保存了Monitor的地址**
![image 1 15.png](JUC笔记/image115.png)
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(==重量级==)之后,该**对象头**的Mark Word 中就被设置指向 Monitor 对象的指针
![image 2 8.png](JUC笔记/image28.png)
**synchronized的3种状态**
- 线程与对象绑定,是在线程中使用对象锁,而不是调用对象
### 偏向锁
- 偏向锁从 Java 6 引入,在 Java 15 被废弃,Java 16-17 默认关闭,最终在 Java 18 中被完全移除
- 17中,不使用锁就是默认状态,使用就是轻量锁
- **默认是偏向锁**(对象头后3位:101),解决锁重入问题,即只有一个线程多次获取(已经获取)锁对象的情况
- 锁升级
- 当调用**hashcode**方法时,由于偏向锁代替了hash值的位置,对象头会恢复hash值正常,所以将锁升级为轻量级锁
- 当多个线程使用锁时(以该对象为锁,错开使用,无竞争),锁期间为轻量锁
- 调用**wait/notify**时,这是重量级锁的方法
- 批量重偏向:当同一个类的**多个对象频繁地发生重偏向时**(重偏向阈值:20),JVM 将超出阈值的对象==偏向锁重置==,而不是升级为轻量锁
- 详解
- 当锁对象与其他线程绑定,此时锁变为轻量级锁(00),解除时,变为初始状态(001),当多个相同类的对象修改绑定(集合中的元素),从第20个开始,对象进行重偏向(101),将与新的线程绑定
- 101(偏向锁) ——> 001(无状态) 101(偏向锁) ——> 101(偏向锁)
> [!important] 总结:批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
- 批量撤销:当一个类的所有对象进行了40次重偏向时,那么这个类下的对象再偏向、创建都会赋默认状态(001)
### 轻量级锁
- 高版本jdk默认是轻量级锁(对象头后2位:00),当一个对象锁**没有竞争**,那么这个锁就是轻量级锁,锁被占用(发生竞争)升级为重量级锁
- 详情
- 在即将开始执行同步代码块中的内容时,会首先检查对象的Mark Word,查看锁对象是否被其他线程占用,如果没有任何线程占用,那么会在当前线程中所处的栈帧中建立一个名为锁记录(Lock Record)的空间,用于复制并存储对象目前的Mark Word信息(官方称为Displaced Mark Word)。接着,虚拟机将使用**CAS操作**将对象的Mark Word更新为轻量级锁状态(数据结构变为指向Lock Record的指针,指向的是当前的栈帧)
- 如果CAS操作失败了的话,那么说明可能这时有线程已经进入这个同步代码块了,这时虚拟机会再次检查对象的Mark Word,是否指向当前线程的栈帧,如果是,说明不是其他线程,而是当前线程已经有了这个对象的锁,直接放心大胆进同步代码块即可。如果不是,那确实是被其他线程占用了。
- 这时,轻量级锁一开始的想法就是错的(这时有对象在竞争资源,已经赌输了),所以说只能将锁膨胀为重量级锁,按照重量级锁的操作执行(注意锁的膨胀是不可逆的)
![image 3 7.png](JUC笔记/image37.png)
### 重量级锁
- 当对象锁,发生竞争时,对象锁要关联一个Monitor对象,管理锁的状态
**自旋优化**
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。说白了就是,等一下
- 详情
| | | |
|---|---|---|
|线程 1 core 1 上)|对象 Mark|线程 2 core 2 上)|
|-|10(重量锁)|-|
|访问同步块,获取 monitor|10(重量锁)|-|
|成功(加锁)|10(重量锁)|-|
|执行同步块|10(重量锁)|-|
|执行同步块|10(重量锁)|访问同步块,获取 monitor|
|执行同步块|10(重量锁)|自旋重试|
|执行完毕|10(重量锁)|自旋重试|
|成功(解锁)|01(无锁)|自旋重试|
|-|10(重量锁)|成功(加锁)|
|-|10(重量锁)|执行同步块|
|-|||
## 保护性设计模式
**join原理**
![image 4 7.png](JUC笔记/image47.png)
- 当线程结束时,会调用notifyAll方法,释放资源和锁
### park&unpark
- 基本使用:它们是 `**LockSupport**` 类中的**静态方法**
```Java
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
```
- 正常:先 `park` 再 `unpark` ,但是,先`unpark`再`park`也可以,不过`park`将暂停不了
- 特点:
- 与 `Object` 的 `wait` & `notify` 相比`wait``notify` 和 `notifyAll` 必须配合 `Object Monitor` 一起使用,而 `park``unpark` 不必
- `park` & `unpark` 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【**精确**】
- `park` & `unpark` 可以先 `unpark`,而 `wait` & `notify` 不能先 `notify`
- **原理**
- 具体是靠c/c++实现,每个线程都有自己的一个Parker对象,由三部分组成`_counter`、`_cond`和`_mutex`
- `_counter` :计数,1:表示线程继续,0:表示线程暂停
- `_cond` : 条件变量,当线程暂停时存储在这里
- `_mutex` 互斥锁
- 小故事理解
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。`_counter`就好比背包中的备用干粮(0 为耗尽,1 为充足)
- 调用 `park`就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用`unpark`,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 `park`时,仅是消耗掉备用干粮,不需停留继续前进因为背包空间有限,多次调用 `unpark`仅会补充一份备用干粮
![image 5 6.png](JUC笔记/image56.png)
park方法
- 当线程调用`**park**`方法,检查`_counter`的值
- 为0,则线程获取`_mutex`互斥锁,进入`_cond`条件变量,阻塞线程,设置`_counter=0`
- 为1,则线程无需阻塞,继续运行,设置`_counter=0`
![image 6 4.png](JUC笔记/image64.png)
unpark方法
- 当线程调用`**unpark**` 方法,将`_counter=1` ,并尝试唤醒 `_cond`条件变量中的线程
- `_cond` 中有线程,则恢复运行,设置 `_counter=0`
- `_cond` 中无线程,`_counter` 值不变
- 总结:park方法,一定会将`_cond` 值设置为0,unpark方法,无阻塞线程时,才能将`_cond` 值设置为1
## 活跃性问题
### 死锁
- 一个线程需要同时获取多把锁,这时就容易发生死锁
- 解决方式
- 按序加锁
- 超时释放锁:使用ReentrantLock
- 使用工具检测是否死锁
- 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
### 活锁
- 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
### 哲学家就餐问题
- 有五位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着,自己就得等待
![image 7 4.png](JUC笔记/image74.png)
## ReentrantLock
- 可重入锁(ReentrantLock) 比synchronized更灵活,体现在可以不用包裹整个代码块中
```Java
synchronized(){
if(){
//内容
}
}
```
```Java
lock.lock();
if(){
//内容
lock.unlock();
}
```
- 可重入锁(嵌套自己🔒)
- `lock.lock()`
- **可打断**(阻塞状态)
- 使用`lock.lockInterruptibly()` ,允许线程在等待获取锁的过程(阻塞状态)中被打断,普通锁无法打断,打断后报异常
- 当线程获取`lockInterruptibly`锁后,则与`lock`方法效果一样,此时其他线程调用`thread.interrupt()`方法,无法中断线程
- **锁超时**(谦让锁)
- `lock.tryLock()` ,返回布尔值,表示是否获取到锁
- `lock.tryLock(long timeout, TimeUnit unit)` ,返回布尔值,表示是否获取到锁,可打断
- 公平锁
- 默认是不公平锁,创建实例时,设置为公平锁(true)
- 一般不设置,影响性能
- **条件变量**(线程通信)
- 一个ReentrantLock实例可以创建**多个条件变量**,条件变量相等于`WaitSet`区域,使线程等待更加细分,不用多个线程在一个区域
- 使用:要先获得锁,在对应条件变量中等待
```Java
//创建条件变量(休息室)
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
//线程首先获取锁
lock.lock();
//进入等待状态,也可以设置时间
condition1.await();
//叫醒等待线程
condition1.signal();
condition1.signalAll();
```
- wait/notify→await/signal
## 内存
### java内存模型
- 原理详解
因为 java是对硬件虚拟化,所有**对 ,以及编译器、CPU指令重排机制这些提示效率的方式进行了实现**,所以定义了一套规范,抽象了线程和主内存之间的关系,规定了从 Java 源代码到 CPU 可执行指令,的这个转化过程要遵守哪些和并发相关的原则和规范,主要目的是为了简化多线程编程,增强程序可移植性的
- JMM(java Memory Model)JMM 是一种规范,定义了 Java 程序中各个变量(包括实例字段、静态字段和数组元素)的访问方式。它通过抽象的主内存(堆)和工作内存(栈)模型,确保线程间的内存**可见性**和**有序性**
- 了解java内存规范,有利于开发者编写**线程安全的程序**
- JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到**线程上下文切**换的影响
- 可见性 - 保证指令不会受 **cpu 缓存**的影响
- 有序性 - 保证指令不会受 **cpu 指令并行优化**的影响
### 可见性
- 现象:当一个线程对公共变量进行修改,另一个线程中数据无法同步(线程不安全)
```Java
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
```
- 循环中有 `System.out.println()`输出语句也可以退出线程,因为输出语句中用到了锁
- 产生原因:t线程频繁读取run的值,**JIT编译器将run的值写入t的工作内存中的缓冲区(**模仿CPU高速缓冲区),其他线程修改的是主内存的值,所以导致数据不一致
![image 8 4.png](JUC笔记/image84.png)
- 解决方式:**[[JUC]]、s**ynchronized
- 使用场景:仅用在**一个写线程,多个读线程**的情况
> [!important] synchronized 语句块既可以**保证代码块的原子性**,也同时**保证代码块内变量的可见性**。但缺点是synchronized 是属于重量级操作,性能相对更低
>
> - 使用synchronized锁,JVM 会插入内存屏障指令。这些内存屏障会**刷新本地线程的缓存**,确保线程可以**看到共享变量的最新值**
### 有序性
- Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内存系统重排** 的过程,最终才变成操作系统可执行的指令序列。**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。
- 现象:**双重检查实现对象单例**无法保证单例
```Java
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
```
无法保证单例的原因是这条语句`**uniqueInstance = new Singleton();**` ,也就是new对象的过程,
- new对象对应的指令:1、创建对象 2、调用构造方法(初始化)3、赋值给静态变量
```Java
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
//堆内创建对象,返回地址存入操作数栈
//复制一份地址,存入操作数栈
//调用构造方法,地址出栈
//将地址赋给静态变量,地址出栈
```
- 由于指令重排,导致先3,后2,当其他线程进入方法时,`if (uniqueInstance == null)` 不满足,因为此时静态变量被赋值为未初始化对象,所以**其他线程可能会获得未初始化的对象**,使用该对象就可能触发空指针异常
- 所以,当在多线程环境下多数据的修改会出现难以预料的情况!
- 解决方式:**[[JUC]]、s**ynchronized
### **volatile**
- 是一个**修饰符**,修饰成员变量(多线程环境),它可以避免线程访问时,访问工作内存(缓存)中的值,==**直接访问主内存中的值**==**,**保证了==**可见性**==,它还可以==**禁止指令重排**==,保证了==**有序性**==
- volatile 的底层实现原理是**内存屏障**Memory BarrierMemory Fence
- 对 volatile 变量的(赋值)写指令后会加入写屏障
- 写屏障(sfence)保证在==**写屏障之前的指令不进行重排**==**,并且**==**对共享变量的改动,都同步到主存中**==
- 对 volatile 变量的(读取)读指令前会加入读屏障
- 读屏障(lfence)保证在==**读屏障之后的指令不进行重排**==**,并且**==**对共享变量的读取,加载的主存中最新数据**==
> [!important] 重要的是要注意,synchronized 并不阻止块内部的指令重排,volatile 提供了更严格的重排序规则,
>
> **它不允许 volatile 变量的读写与其他内存操作重排**,synchronized 的重排序规则相对没那么严格
### Happens-before
- 是 Java 内存模型(JMM)中的一个核心**概念**,用于定义操作之间的内存可见性保证。它是一种偏序关系,用来**描述程序中不同操作的执行顺序和可见性**
- 定义:如果一个操作 A happens-before 另一个操作 B,那么 ==**A 的结果对 B 是可见的**==,且 A 的执行顺序排在 B 之前
- 主要作用:
- 确保内存可见性:保证一个线程的写操作对其他线程是可见的。
- 防止重排序:限制编译器和处理器对指令进行重排序的能力
- 常见情况
a. 程序顺序规则:
在同一个线程中,按照程序的顺序,前面的操作 happens-before 后面的操作。
b. 监视器锁规则:
一个锁的解锁 happens-before 于后续对这个锁的加锁。
c. volatile 变量规则:
对一个 volatile 变量的写操作 happens-before 于后续对这个变量的读操作。
d. 线程启动规则:
Thread 对象的 start() 方法 happens-before 于这个线程的每一个动作。
e. 线程终止规则:
线程中的所有操作都 happens-before 于其他线程检测到这个线程已经终止。
f. 中断规则:
一个线程调用另一个线程的 interrupt() 方法 happens-before 于被中断线程检测到中断事件的发生。
g. 终结器规则:
一个对象的初始化完成 happens-before 于它的 finalize() 方法的开始。
h. 传递性:
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
## 无锁
### CAS
- 使用`CAS`机制解决并发修改问题
```Java
//修改共享数据
AtomicInteger balance;//封装的共享数据
while(true) {
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) break;
}
```
- Compare And Swap,它是一种用于实现多线程环境下的**无锁并发**编程技术,是基于 的思想,不怕共享变量被修改,保障每一次修改是成功的(原子修改),无锁并发、无阻塞并发,
- 底层 ,封装数据对应的变量,由volatitle修饰
- CAS 的底层是`lock cmpxchg` 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性
- 效率问题:因为线程需要一直运行(只有就绪、运行状态),所以线程数小于cpu核心数时才能提升效率
- 使用
**原子整数**
- 对基本数据类型进行了封装,比如AtomicBoolean、AtomicInteger、AtomicLong使用类似,以AtomicInteger为例
- 构造方法:
- 常用方法:
| | |
|---|---|
|方法名|描述|
||获取值|
||修改数据,返回是否修改成功,可能修改失败|
||返回自增后的值/返回值再自增|
|||
||返回增加data后的值/返回值再增加data|
|||
||修改数据|
**原子引用**
- 对引用对象进行封装,如AtomicReference、AtomicMarkableReference、AtomicStampedReference,每次修改对象时,**更新的实际上是对象的引用地址**
上同
AtomicReference的·使用·
- ABA问题:线程无法感知数据被修改(指修改完,值不变),可以使用版本号记录
AtomicStampedReference基于**版本号**的方法,每次修改需要修改版本号,告知共享数据是否被修改
AtomicMarkableReference维护一个**布尔值**表示状态,只关心数据是否被修改而不看次数
**原子数组**
- 因为原子引用每次修改的都是地址,而数组的修改是对内容的修改,所以推出原子数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
字段更新器
原子累加器
### 🚀 什么是 CAS
**Compare And Swap** 是一种原子操作,用于在多线程环境中安全地更新变量,避免使用互斥锁(`synchronized` 或 `mutex`)造成的性能瓶颈。
它的基本思路就是:
> “我想把一个值改成新值,但前提是它现在还是旧值。”
---
### 🔧 CAS 的原理
假设你有一个变量 `V`,你想把它从旧值 `A` 改成新值 `B`CAS 会做这样的操作:
```Plain
if (V == A) {
V = B;
return true; // 修改成功
} else {
return false; // 修改失败,有别的线程动过它
}
```
这个操作是 **原子的**(不会被中断),通常由硬件指令(如 x86 的 `CMPXCHG`)直接支持。
---
### 📦 应用场景
- Java 中的 `AtomicInteger`, `AtomicReference` 等类底层就是用 CAS 实现的。
- 实现无锁数据结构(如无锁队列、无锁栈)
- 避免使用传统的锁,提高并发性能
---
### ⚠️ CAS 的问题
1. **ABA 问题**
- 如果变量从 A 改成 B,然后又改回 A,CAS 认为它没变,其实发生了两次变化。
- 解决方法:使用 **版本号** 或 **带时间戳的引用**,比如 Java 的 `AtomicStampedReference`。
2. **自旋问题**
- 如果一直 CAS 失败(比如多个线程争抢同一个变量),会反复尝试,造成高 CPU 占用。
3. **只能操作一个变量**
- 无法同时原子地更新多个变量,除非封装成一个整体结构。
---
## 悲观锁、乐观锁
### 悲观锁
- 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
- 像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。
### 乐观锁
- 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
- 在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。