前言在并发编程中解决共享资源线程安全主要有两大思想悲观锁、乐观锁悲观锁顾名思义就像是一个悲观的人总会觉得别人会修改数据所以在操作数据之前先把数据数据锁住自己独占使用其他线程必须等到我释放之后才可以使用Java中的synchronized就是悲观锁的体现之前有文章详细的介绍虽然会保证线程安全但是会带来线程阻塞、上下文切换、并发吞吐量低等问题乐观锁就是一个乐观的人觉得别人不会同时修改数据所以不对数据加锁直接操作数据更新时在检查有没有人动过数据若没有人动就更新成功如果有人动就放弃本次修改或重试相比于悲观锁更适合高并发场景一、乐观锁核心思想1.定义乐观锁是一种无锁并发控制思想默认认为多线程之间很少发生数据竞争不上锁、不阻塞直接读写共享数据在提交更新的那一刻校验数据是否被其他线程修改过若未被修改更新成功若已被修改放弃本次更新或自旋重试。2.核心特征不加独占锁无线程阻塞无队列等待事后校验读的时候不校验更新时才校验冲突自愈发现竞争冲突不阻塞通过自旋重试解决最终一致性不保证瞬时强一致保证最终数据一致。3.与悲观锁区别悲观锁先加锁→再操作悲观预判必有竞争独占资源乐观锁先操作→后校验乐观预判少有竞争无锁并行。更详细的对比见文章末尾二、乐观锁两大主流实现方式1.CAS无锁实现Java内存并发常用全称Compare And Swap比较并交换是 CPU 硬件级支持的原子指令也是 Java 中乐观锁的底层基石。详细看第三点2.版本号机制数据库分布式并发常用数据表增加version版本字段更新时携带旧版本号匹配不匹配则更新失败。详细看第五点三、CAS算法Java内存并发常用1.定义是CPU 硬件级支持的原子指令也是 Java 中乐观锁的底层基石。思想就是用一个预期值和要更新的变量值进行比较两值相等才会进行更新2.三个核心参数CAS(V, E, N)V (Variable)内存中共享变量的实际值E (Expected)线程预期原值自己读取到的值N (New)想要修改的新值3.执行逻辑原子不可分割当且仅当V的值等于E时CAS通过原子方式用新值N来更新V的值如果不等说明已经有其他线程更新了V则当前线程放弃更新举一个简单的例子线程A要改变变量 i 的值为6i 原值为1V1,E1,N6i与1进行比较如果相等则说明没有被其他线程修改可以设置为6如果不相等则说明被其他线程修改当前线程放弃更新当多个线程同时使用CAS操作一个变量时只有一个会胜出并成功更新其余都会失败4.CAS为什么能实现乐观锁全程没有加锁、没有阻塞依靠读取原值 → 运算 → CAS 校验更新三步失败后不阻塞循环重试自旋直到更新成功。5.CPU底层支撑CAS 不是操作系统实现也不是 Java 语言实现依赖 CPU 总线锁 / 缓存锁多核 CPU 下通过缓存行锁定保证 CAS 指令原子性避免多线程同时修改同一缓存行数据从硬件层面保障操作不可分割。四、Java中CAS源码剖析JUC 包下原子类全部基于 CAS 实现乐观锁AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等。1.Atomiclnteger自增底层AtomicInteger count new AtomicInteger(0); count.incrementAndGet();底层核心源码JDK8public final int incrementAndGet() { return getAndAdd(1) 1; } public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); }里面涉及到的Unsafe 类Java 底层不安全类可以直接操作内存、调用 CPU 原子指令valueOffset变量在内存中的偏移地址直接定位内存数据。Unsafe本地方法自选逻辑:核心流程循环读取内存最新值、尝试CAS更新、失败就继续循环自旋乐观锁直到成功public final int getAndAddInt(Object o, long offset, int delta) { int v; // 自旋循环CAS失败就一直重试 do { // 从内存中读取当前最新值 v getIntVolatile(o, offset); // CAS比较并交换内存值是否等于v是则delta否则循环重试 while (!compareAndSwapInt(o, offset, v, v delta)); return v; }2.volatile配合CAS的作用原子类中变量都被volatile修饰volatile 保证可见性线程每次都从主内存读最新值不从工作内存缓存读volatile 不保证原子性所以必须靠 CAS 补全原子操作组合volatile CAS实现无锁原子并发。private volatile int value;五、版本号机制数据库场景1.实现思路1数据库表新增字段version int default 12查询数据时同时查出当前version3更新数据时必须携带旧版本号作为条件4更新成功则版本号 1版本不匹配则更新失败。2.SQL示例1查询select id, name, version from user where id 1;2更新update user set name 新名称, version version 1 where id 1 and version 旧版本号;3.执行逻辑线程 A 查到 version1线程 B 同时查到 version1并抢先更新为 version2线程 A 再用 version1 去更新条件不匹配更新行数为 0失败业务层可选择抛出异常、重试、放弃操作。六、CAS乐观锁三大致命问题1.ABA问题1现象线程 1 读取值 A准备 CAS 更新线程 2 先把 A 改成 B又改回 A线程 1 CAS 发现还是 A认为没被修改正常更新忽略了中间被篡改的过程。2危害会导致数据状态被覆盖业务逻辑出错如资金、库存。3解决方案增加版本号每次修改版本自增即使值变回 A版本也不同JDK 工具类AtomicStampedReference、AtomicMarkableReference携带版本戳 / 标记位不仅比较值还比较版本。2.循环自旋消耗CPU1现象CAS 冲突严重时会无限循环重试一直占用 CPU导致 CPU 飙高。2解决限制自旋次数超过次数改用悲观锁自适应自旋JDK 锁优化思想。3.只能保证单个变量原子性1现象CAS 只能对一个变量做原子更新如果需要同时修改多个共享变量CAS 无法保证原子性。2解决使用AtomicReference封装对象把多个变量装进一个对象改用悲观锁ReentrantLock/synchronized。七、乐观锁 VS 悲观锁对比层面乐观锁悲观锁加锁思想无锁事后校验独占锁事前加锁底层实现CAS、版本号synchronized、ReentrantLock线程状态自旋重试不阻塞阻塞挂起让出 CPU并发性能高无阻塞开销低有上下文切换一致性最终一致强一致性开销竞争小开销低竞争大耗 CPU加锁、阻塞、切换开销固定典型场景读多写少、缓存、计数写多读少、资金交易、库存扣减