乐观锁、悲观锁、并发处理、下单、支付等、violate、synchronized、超卖、少卖 文章目录实现机制对比适用场景对比乐观锁一定能拿到结果吗?自旋乐观锁的两种方式及选择版本号方式原子sql方式版本号vs原子sql乐观锁实现类 Atomic系列的类synchronized可以加在哪里?锁的是什么?是方法还是变量。锁的四种状态重量级锁什么时候会升级到重量级锁?支付支付场景的一个可实现方案超卖少卖重新上架需要做哪些事情violate可以解决并发问题吗?为什么?乐观锁、悲观锁听的耳朵都磨出茧子了。他们都属并发处理(并发控制的范畴)。实现机制对比—乐观锁悲观锁实现机制1、cas机制。2、版本号机制。加锁。1、java代码锁。2、数据库锁(如for update)注乐观锁如引入版本号需要添加字段这是其成本之一。适用场景对比维度乐观锁悲观锁—数据一致性不是特别高的场景因为有一定(虽然很低)的失误率数据一致性要求高的场景如金融、医疗等乐观锁一定能拿到结果吗?不一定高并发情况下多线程进来有可能正确的线程永远拿不到版本号造成cpu空转。那么重试3次可以解决问题吗?可以作为常规兜底方案避免cpu空转。但是高并发场景下这并不能从根因上解决问题还是会争抢并可能造成大量失败单影响用户体验。要彻底解决要上组合拳了这又是另外一个范畴的问题了。按工程落地优先级从高到低分为四大类打散热点 削峰排队 数据库层串行 上层缓存预计算。自旋自旋的本质是一个死循环 CAS 操作伪代码如下// 线程 B 尝试获取锁while(true){// 1. 读取对象头的 Mark Word// 2. CAS 操作尝试将 Mark Word 从线程A持有 改为 线程B持有if(CAS(MarkWord,expectedValue,newValue)){break;// 成功了获得锁}// 3. 失败继续循环再来一次这就是自旋}乐观锁的两种方式及选择乐观锁有两种1、版本号2、原子sql版本号方式实现方式给数据加一个version字段每次更新时检查版本号是否与读取时一致一致才允许更新同时版本号1。示例1.查询时记录版本号例如 读到 stock10,version5SELECTstock,versionFROMproductWHEREid1;--2.更新时带上版本号条件如果affectedrows0说明期间已被其他事务修改需要重试UPDATEproductSETstock7,versionversion1WHEREid1ANDversion5;原子sql方式实现方式把计算逻辑下推到数据库用一条 SQL 原子完成读旧值→计算→写新值利用数据库行锁保证串行执行。示例一条SQL搞定不需要先查。如果affectedrows0说明库存不足。UPDATEproductSETstockstock-#{quantity}WHEREid#{id} AND stock #{quantity};版本号vs原子sql维度版本号原子SQL防并发覆盖能防能防防ABA问题能防天然不存在基于数据库最新值计算需要重试冲突时需要重试高并发下可能重试风暴不需要排队执行即可高并发表现冲突率高大量请求失败重试串行排队都能成功改表结构需要加version字段不需要适用场景通用字段修改如多人编辑文档、修改配置项普通字段无法用原子sql数量累加/扣减如库存、余额、积分实现复杂度较高需要重试逻辑较低一条SQL搞定注原子sql根本没有aba的场景因为数据本身就是在数据库判断的。还有一点千万别别错说为普通字段只能用版本号这是不对的。普通字段无法用原子sql # 正确普通字段只能用版本号 # 错误因为还可以用for update等方式乐观锁实现类 Atomic系列的类synchronized可以加在哪里?加在方法(实例方法)上加在静态方法上加在对象上加在Class上 # 相当于锁static注int等不可以加锁如果是Integer可以加锁但极度不推荐。锁的是什么?是方法还是变量。锁的是对象具体来说是对象header的markdown。object(对象)结构为object header # 对象头 mark word #包含(锁、hash、GC年龄、Monitor指针)偏向线程id # 偏向锁标志位 # 是否为偏向锁 锁标志位 # 无锁、或偏向锁获取锁的动作相当于尝试将mark word的threadId修改为要进入的线程。当对象升级为重量级锁时会为该对象创建monitorheader里面存monitor的指针。monitor管理器(并不属于对象)通过monitor指针管理对象。锁的四种状态JVM为了优化性能会随着竞争激烈程度让synchronized锁自动升级。它一共有4种状态按级别从低到高分别是1、无锁状态(刚创建对象没被任何线程访问)2、偏向锁(只有一个线程反复进入)3、轻量级锁(多个线程交替进入但没冲突)4、重量级锁(多个线程激烈争抢阻塞等待)关键点这四种状态都是synchronized在运行过程中可能呈现的样子。synchronized并不固定等于其中某一种它会根据情况自动“膨胀”(升级)。JVM执行时是这样演变的。1、第一个线程A调用JVM发现目前锁是“无锁”状态。因为现在只有A自己在跑JVM直接开启“偏向锁模式”在对象头记录ThreadIDA。此时这个synchronized锁处于“偏向锁”状态。2、线程A再次调用JVM看到对象头记录的是A直接放行。不需要任何CAS操作耗时几乎为0。这个synchronized依然处于“偏向锁”状态。3、线程B来调用竞争开始JVM发现对象头记录的是A但现在是B要进来。偏向锁失效JVM会执行“偏向锁撤销”。锁状态立刻升级为“轻量级锁”通过CAS自旋获取。此时这个synchronized锁变成了“轻量级锁”状态。4、线程C、D同时冲进来激烈竞争轻量级锁自旋等待时间过长。锁状态再次升级为“重量级锁”由操作系统调度。此时这个synchronized锁变成了“重量级锁”状态。重量级锁重量级锁是锁的一种状态。什么时候会升级到重量级锁?轻量级锁升级为重量级锁并没有一个单一的、固定的硬性条件而是一套由JVM动态判断的综合决策机制。这套机制的核心目标是在CPU 空转消耗与线程阻塞切换开销之间找到一个平衡点。可能的情况。1、自旋次数/时间超限这是最直接的触发条件。(1)固定阈值(历史版本)在早期的JDK中有一个固定的自旋次数阈值默认是10次。可以通过JVM参数-XX:PreBlockSpin调整。一旦自旋尝试获取锁的次数超过这个值就会升级。(2)自适应自旋(JDK1.6默认)这是目前JVM的主流行为。JVM不再使用固定次数而是根据历史上在同一个锁上自旋的成功率来动态决定自旋时间。如果上次自旋成功这次就多自旋一会儿如果很少成功则可能直接跳过自旋快速升级避免浪费CPU。2、自旋线程数过多当有大量线程同时自旋等待同一把锁时即便单次自旋不成功这些线程加起来对CPU的消耗也是巨大的。在某些资料中提到如果自旋的线程数量超过了CPU核心数的一半可能会触发升级。例如8核CPU有5个线程在自旋JVM就可能认为得不偿失而选择升级。3、锁被长时间占用如果当前持有锁的线程迟迟不释放锁(比如在执行I/O操作或复杂计算)其他线程再怎么自旋也等不到。JVM会监控到这种情况为了避免CPU被无意义地消耗会主动将锁升级为重量级锁让等待的线程进入阻塞状态释放CPU资源。支付支付场景的一个可实现方案提示信息为正在提交订单当前第5位共5位。# 每1秒该提示刷新一次见过有平台这么做。用一个队列限流虽然不是很快但是效率还可以。超卖防止超卖的核心是绝对拦截。一般都是三段式redis(lua) # 预减库存mq # 两大作用 1、将创建订单请求发给mq异步处理 2、物流、积分等消息db # 最终一致性少卖防止少卖的核心是精准释放。一般是两段式延迟队列 # 例如15分钟后检查订单状态定时扫描 # 如果mq出现问题还可以通过扫描订单表状态为未支付的进行处理重新上架需要做哪些事情建议先下架再上架错开点时间避免单子还没跑完。上架后需要刷新redis因为一般都是用redis来实现预减库存的。violate可以解决并发问题吗?为什么?不可以。多线程情况下violate可以保证可见性但是无法保证原子性。如果想解决并发问题可以结合violate和synchronized一起来实现。有一个例子比较好1、读取i# violate只在这一步有效2、tempi1;3、itemp;如上假设值的变化分为3步violate只在第一步有效如果a、b两个线程a线程修改了i的值但是b线程已过了读取的步骤那么还是会有并发问题。Java中violate关键字详解2真正了解violate # 还可以吧