29 KiB
title, date, tags
| title | date | tags | |
|---|---|---|---|
| JUC笔记 | 2024-08-25 |
|
- JUC:java.util.concurrent 并发编程
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类(juc)
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。但是自己手动组合方法是线程不安全的
[!important] String、Integer是不可变线程,因为值是不可修改
效率问题
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用
cpu ,不至于一个线程总占用 cpu,别的线程没法干活 - 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任
务都能拆分(参考后文的【阿姆达尔定律】)
也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义 - IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一
直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
注意
需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
原子性
-
原子操作可以保证原子性,加锁本质是保证了多线程修改操作为原子操作
- 原子操作:一个操作或者多个操作组合在一起,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。特点:不可打断、一致性
-
两个线程对一个变量进行修改,产生线程问题就是因为修改不是原子操作
例如对于 i++、i-- 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
//i++操作 getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i //i--操作 getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i多线程,指令交错执行
// 假设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对象头:内存结构
-
普通对象
|--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------| -
数组对象
|---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------| -
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------| | 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 | |-------------------------------------------------------|--------------------||--------------------------------------------------------------------|--------------------| | 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的地址)
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
-
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(==重量级==)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
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(偏向锁)
- 当锁对象与其他线程绑定,此时锁变为轻量级锁(00),解除时,变为初始状态(001),当多个相同类的对象修改绑定(集合中的元素),从第20个开始,对象进行重偏向(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,是否指向当前线程的栈帧,如果是,说明不是其他线程,而是当前线程已经有了这个对象的锁,直接放心大胆进同步代码块即可。如果不是,那确实是被其他线程占用了。
- 这时,轻量级锁一开始的想法就是错的(这时有对象在竞争资源,已经赌输了),所以说只能将锁膨胀为重量级锁,按照重量级锁的操作执行(注意锁的膨胀是不可逆的)
重量级锁
- 当对象锁,发生竞争时,对象锁要关联一个Monitor对象,管理锁的状态
自旋优化
-
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。说白了就是,等一下
-
详情
线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上) - 10(重量锁) - 访问同步块,获取 monitor 10(重量锁) - 成功(加锁) 10(重量锁) - 执行同步块 10(重量锁) - 执行同步块 10(重量锁) 访问同步块,获取 monitor 执行同步块 10(重量锁) 自旋重试 执行完毕 10(重量锁) 自旋重试 成功(解锁) 01(无锁) 自旋重试 - 10(重量锁) 成功(加锁) - 10(重量锁) 执行同步块 -
保护性设计模式
join原理
- 当线程结束时,会调用notifyAll方法,释放资源和锁
park&unpark
-
基本使用:它们是
**LockSupport**类中的静态方法// 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象) -
正常:先
park再unpark,但是,先unpark再park也可以,不过park将暂停不了 -
特点:
- 与
Object的wait¬ify相比wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必 park&unpark是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】park&unpark可以先unpark,而wait¬ify不能先notify
- 与
-
原理
-
具体是靠c/c++实现,每个线程都有自己的一个Parker对象,由三部分组成
_counter、_cond和_mutex_counter:计数,1:表示线程继续,0:表示线程暂停_cond: 条件变量,当线程暂停时存储在这里_mutex: 互斥锁
-
小故事理解
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。
_counter就好比背包中的备用干粮(0 为耗尽,1 为充足) - 调用
park就是要看需不需要停下来歇息- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用
unpark,就好比令干粮充足- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用
park时,仅是消耗掉备用干粮,不需停留继续前进因为背包空间有限,多次调用unpark仅会补充一份备用干粮
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。
park方法
- 当线程调用
**park**方法,检查_counter的值- 为0,则线程获取
_mutex互斥锁,进入_cond条件变量,阻塞线程,设置_counter=0 - 为1,则线程无需阻塞,继续运行,设置
_counter=0
- 为0,则线程获取
unpark方法
- 当线程调用
**unpark**方法,将_counter=1,并尝试唤醒_cond条件变量中的线程_cond中有线程,则恢复运行,设置_counter=0_cond中无线程,_counter值不变
-
-
总结:park方法,一定会将
_cond值设置为0,unpark方法,无阻塞线程时,才能将_cond值设置为1
活跃性问题
死锁
-
一个线程需要同时获取多把锁,这时就容易发生死锁
-
解决方式
-
按序加锁
-
超时释放锁:使用ReentrantLock
-
使用工具检测是否死锁
- 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
-
活锁
- 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
哲学家就餐问题
-
有五位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着,自己就得等待
ReentrantLock
- 可重入锁(ReentrantLock) 比synchronized更灵活,体现在可以不用包裹整个代码块中
synchronized(){
if(){
//内容
}
}
lock.lock();
if(){
//内容
lock.unlock();
}
-
可重入锁(嵌套自己🔒)
lock.lock()
-
可打断(阻塞状态)
- 使用
lock.lockInterruptibly(),允许线程在等待获取锁的过程(阻塞状态)中被打断,普通锁无法打断,打断后报异常 - 当线程获取
lockInterruptibly锁后,则与lock方法效果一样,此时其他线程调用thread.interrupt()方法,无法中断线程
- 使用
-
锁超时(谦让锁)
lock.tryLock(),返回布尔值,表示是否获取到锁lock.tryLock(long timeout, TimeUnit unit),返回布尔值,表示是否获取到锁,可打断
-
公平锁
- 默认是不公平锁,创建实例时,设置为公平锁(true)
- 一般不设置,影响性能
-
条件变量(线程通信)
-
一个ReentrantLock实例可以创建多个条件变量,条件变量相等于
WaitSet区域,使线程等待更加细分,不用多个线程在一个区域 -
使用:要先获得锁,在对应条件变量中等待
//创建条件变量(休息室) 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 指令并行优化的影响
可见性
-
现象:当一个线程对公共变量进行修改,另一个线程中数据无法同步(线程不安全)
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高速缓冲区),其他线程修改的是主内存的值,所以导致数据不一致
-
解决方式:JUC、synchronized
-
使用场景:仅用在一个写线程,多个读线程的情况
[!important] synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低
- 使用synchronized锁,JVM 会插入内存屏障指令。这些内存屏障会刷新本地线程的缓存,确保线程可以看到共享变量的最新值
有序性
-
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、赋值给静态变量
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、synchronized
volatile
- 是一个修饰符,修饰成员变量(多线程环境),它可以避免线程访问时,访问工作内存(缓存)中的值,==直接访问主内存中的值==**,**保证了==可见性==,它还可以==禁止指令重排==,保证了==有序性==
- volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的(赋值)写指令后会加入写屏障
- 写屏障(sfence)保证在==写屏障之前的指令不进行重排==,并且==对共享变量的改动,都同步到主存中==
- 对 volatile 变量的(读取)读指令前会加入读屏障
- 读屏障(lfence)保证在==读屏障之后的指令不进行重排==,并且==对共享变量的读取,加载的主存中最新数据==
- 对 volatile 变量的(赋值)写指令后会加入写屏障
[!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机制解决并发修改问题//修改共享数据 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 下都能够保证【比较-交换】的原子性
- CAS 的底层是
-
效率问题:因为线程需要一直运行(只有就绪、运行状态),所以线程数小于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 会做这样的操作:
if (V == A) {
V = B;
return true; // 修改成功
} else {
return false; // 修改失败,有别的线程动过它
}
这个操作是 原子的(不会被中断),通常由硬件指令(如 x86 的 CMPXCHG)直接支持。
📦 应用场景
- Java 中的
AtomicInteger,AtomicReference等类底层就是用 CAS 实现的。 - 实现无锁数据结构(如无锁队列、无锁栈)
- 避免使用传统的锁,提高并发性能
⚠️ CAS 的问题
- ABA 问题:
- 如果变量从 A 改成 B,然后又改回 A,CAS 认为它没变,其实发生了两次变化。
- 解决方法:使用 版本号 或 带时间戳的引用,比如 Java 的
AtomicStampedReference。
- 自旋问题:
- 如果一直 CAS 失败(比如多个线程争抢同一个变量),会反复尝试,造成高 CPU 占用。
- 只能操作一个变量:
- 无法同时原子地更新多个变量,除非封装成一个整体结构。
悲观锁、乐观锁
悲观锁
- 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
- 像 Java 中
synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
- 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
- 在 Java 中
java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。








