--- title: JUC笔记 date: 2024-08-25 tags: [JUC] --- - JUC:java.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 "":()V 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; //堆内创建对象,返回地址存入操作数栈 //复制一份地址,存入操作数栈 //调用构造方法,地址出栈 //将地址赋给静态变量,地址出栈 ``` - 由于指令重排,导致先3,后2,当其他线程进入方法时,`if (uniqueInstance == null)` 不满足,因为此时静态变量被赋值为未初始化对象,所以**其他线程可能会获得未初始化的对象**,使用该对象就可能触发空指针异常 - 所以,当在多线程环境下多数据的修改会出现难以预料的情况! - 解决方式:**[[JUC]]、s**ynchronized ### **volatile** - 是一个**修饰符**,修饰成员变量(多线程环境),它可以避免线程访问时,访问工作内存(缓存)中的值,==**直接访问主内存中的值**==**,**保证了==**可见性**==,它还可以==**禁止指令重排**==,保证了==**有序性**== - volatile 的底层实现原理是**内存屏障**,Memory Barrier(Memory 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** 实现的。