【JUC】2、Lock锁机制详解

94 阅读15分钟

一、概念

知识点核心内容关键点/易混淆点
并发编程基础并发编程入门知识,包括线程安全、原子操作等线程安全与原子操作的区别与联系
原子操作通过CPU支持的CAS指令实现原子性CAS指令与synchronized关键字的性能比较
CAS操作比较并交换(Compare and Swap)ABA问题及其解决方案(版本戳)
线程封闭将对象封装到线程内部,实现线程安全局部变量与全局变量的线程安全性
无状态类没有成员变量的类,一定是线程安全的传入对象作为参数时的线程安全性问题
不可变类使用final关键字修饰成员变量,使其不可变final关键字修饰对象时,对象内部数据仍可变
死锁多个进程因竞争资源而造成阻塞现象死锁产生的三个必要条件:互斥、请求和保持、不剥夺和环路等待
活锁线程不断尝试获取锁,但始终无法成功,导致无限循环活锁与死锁的区别:活锁线程处于可运行状态,死锁线程处于阻塞状态
线程饥饿线程因优先级低而始终无法获取CPU执行时间线程饥饿与死锁、活锁的区别
LongAdder通过写热点分散的方式提升原子操作的性能LongAdder与AtomicInteger的性能比较及适用场景

二、实现原理

原子操作
  • 原子操作: 原子操作是指不可分割的操作,即操作在执行过程中不会被其他操作打
  • 举例: 类似于我们平时说的“事物”,事物具有四大特性,即ACID特性,其中的A就是Atomic,表示原子性,即事务中的操作要么全部完成,要么全部不完成,不会停留在中间某个状态。
2)CAS实现原子操作 16:03
  • 原子操作在事务中的体现 16:13
    • 原子性定义: 原子性指的是一个事务中的操作要么全部完成,要么全部不完成,是一个不可分割的整体。
    • 事务中的原子性: 在事务中,从begin开始到commit结束,中间的所有操作在外部看来要么全部执行完成,要么全部未执行(若rollback)。
  • 并发中的原子性 17:07
    • 并发原子性: 在并发编程中,一个线程的操作对于其他线程来说,要么全部可见,要么全部不可见,具有原子性。
    • 例子: 线程A和线程B各自执行一系列操作,对于线程B来说,线程A的操作要么全部执行完,要么全部没执行,反之亦然。
  • 使用锁实现原子性 17:53
    • 锁机制: 使用锁(如synchronized关键字)可以确保一段代码在执行时,其他线程无法进入,从而实现原子性。
    • 实现方式: 将需要原子执行的代码块用锁关键字包围,确保这段代码要么全部执行完,要么不被执行。
  • 锁机制的不足 18:27
    • 阻塞问题: 锁机制会导致未获得锁的线程阻塞,等待锁的释放,可能影响高优先级线程的执行。
    • 锁不释放问题: 如果获得锁的线程在执行过程中出现问题,可能导致锁一直不释放,造成其他线程无限等待。
    • 死锁问题: 锁机制可能导致死锁,即两个或多个线程相互等待对方释放锁。
    • 性能问题: 锁机制是一种比较粗糙、力度较大的操作,对于简单的操作(如累加)来说,加锁的开销可能远大于操作本身的开销。
    • 引入Atomic系列: 为了解决简单操作加锁过于笨重的问题,Java提供了Atomic系列的原子操作类,这些类通过CAS(Compare-And-Swap)机制实现原子操作,无需加锁,性能更高。
3)Jdk中相关原子操作类的使用 20:54
  • 原子操作类的实现机制 21:27
    • 实现机制: Java中的原子操作类是通过利用当前处理器普遍支持的CAS(Compare-And-Swap)指令来实现的。例如,Intel的汇编指令cmpxchg。
    • CAS指令: CAS操作包含三个运算符:一个内存地址V,一个期望的值A和一个新值B。当内存地址V上的值等于期望的值A时,将该地址上的值更新为新值B,否则不进行任何操作。
  • CAS指令与原子操作 22:01
    • CAS指令: CAS指令,如Intel的cmpxchg,是一种CPU指令,用于实现无锁并发控制。不同厂家的具体实现算法可能不同,但原理基本一致。
    • 原子操作: 通过CAS指令,可以保证比较和交换这两个操作是原子的,即在CPU指令级别上不可分割,从而避免了多线程环境下的竞态条件。
    • 应用实例: 在秒杀库存扣减等场景中,使用CAS指令可以保证库存扣减操作的原子性,避免库存超卖等问题。
  • CAS操作过程 25:18
    • 操作过程: CAS操作包含三个关键参数:内存地址V、期望值A和新值B。当地址V上的值等于期望值A时,将地址V上的值更新为新值B;否则,不进行任何操作,并返回原值。
    • 循环CAS: 由于CAS操作可能不成功(例如,当地址V上的值不等于期望值A时),因此在实际应用中,通常会使用循环CAS,即在一个循环中不断尝试CAS操作,直到成功为止。
    • Java中的实现: Java中的Atomic系列原子操作类就是利用了循环CAS来实现无锁并发控制的。
    • 举例: 例如,对变量count进行累加操作,可以将count的内存地址设置为V,原值设置为A,新值设置为B(A+1)。然后,通过CAS指令进行比较和交换,实现count的原子累加。如果CAS操作失败,则继续循环尝试,直到成功为止。
4)CAS指令与原子操作 27:00
  • Atomic系列原子操作类 27:13
  • CAS操作过程与运算符 28:34
  • 循环CAS与原子变量操作 31:02
  • 多线程下的CAS操作示例 31:50
  • CAS操作性能优势 38:00
  • CAS原子性保证 39:06
    • 原子性保证: CAS操作是由操作系统或CPU专门提供的指令,在硬件级别保证了原子性。
    • 原因: 这是由CPU处理器专门提供的支持CAS操作的指令所保证的。
  • Atomic系列类的方法
    • 方法示例:
      • getAndSet(int newValue): 原子地设置为给定值,并返回旧值。
      • compareAndSet(int expect, int update): 如果当前值等于期望值,则原子地设置为新值,并返回成功或失败。
      • getAndIncrement(): 原子地递增当前值,并返回旧值。
    • 底层实现: 这些方法最终都会调用Unsafe类中的compareAndSwap相关方法,这些方法是native的,基于操作系统或CPU指令实现。
5)线程安全性详解 39:42
  • synchronized关键字的局限性
    • 阻塞锁机制: synchronized关键字是基于阻塞的锁机制,一个线程拥有锁时,其他线程需等待。
    • 问题与局限:
      • 高优先级线程被阻塞时无法优先执行。
      • 线程可能长时间不释放锁,导致其他线程长时间等待。
      • 可能出现死锁等复杂情况。
      • 锁机制粒度大,对于简单需求如计数器过于笨重。
  • Atomic系列原子操作类
  • CAS操作的问题
    • ABA问题: CAS操作可能引入ABA问题,即值从A变到B又变回A,但CAS操作无法感知中间的变化。
    • 其他潜在问题: 虽未详细展开,但应留意CAS操作可能带来的其他潜在问题。

CAS与Atomic原子操作解析

  • 原子操作的概念 40:04
    • 原子操作: 原子操作是不可被中断的一个或一系列操作。
  • CAS实现原子操作的三大问题
    • ABA问题
      • ABA问题定义: 在执行CAS操作前,变量的值由A变为B,又变回A,导致CAS操作误以为变量值未变而成功执行。
      • ABA问题示例: 类似于你倒了一杯水,回来时水还在,但中间可能被人喝过,你又拿起来喝,这就是ABA问题。
      • 解决方法: 引入版本号或类似机制,确保在变量值变化时能够检测到。
    • 循环时间长开销大
      • 问题描述: CAS操作需要不断循环比较并尝试更新,如果变量值长时间不变或竞争激烈,会导致循环次数增加,开销增大。
      • 解决方法: 优化算法,减少不必要的循环;或采用其他并发控制机制。
    • 只能保证一个共享变量的原子操作
      • 问题描述: CAS机制通常只能保证对一个共享变量的原子操作,对于多个共享变量的原子性操作需要额外的机制来保证。
      • 解决方法: 采用更复杂的并发控制机制,如锁、事务内存等。
  • 原子操作类的使用
    • AtomicInteger: 用于原子性地更新整数值。
    • AtomicIntegerArray: 用于原子性地更新整数数组中的元素。
    • AtomicReference: 用于原子性地更新引用类型变量。
    • AtomicStampedReference: 带有版本号的原子引用,可以解决ABA问题。
    • AtomicMarkableReference: 带有标记位的原子引用,也可以用于解决某些并发问题。
    • AtomicUpdateField类: 如AtomicIntegerFieldUpdater等,用于原子性地更新对象的某个字段。
    • 总结: 这些原子操作类提供了高效的线程安全操作,避免了传统锁机制带来的性能开销和复杂性。

活锁与线程饥饿

  • 版本戳与ABA问题 42:33
    • 版本戳的定义与作用
      • 版本戳定义: 版本戳是一个用于标记数据版本或状态的标识符,通过在数据值后附加一个额外的标志或变量(如数字)来实现。
      • 作用: 版本戳用于解决并发编程中的ABA问题,即当某个值在比较和交换(CAS)操作期间被其他线程修改并又改回原值,但实际上已不是期望的原始状态。
      • 示例说明: 初始值为a,存放时加上版本戳变为a1。若线程将其改为c后再改回a,此时版本戳变为a3。CAS操作时,期望值为a1,但实际为a3,因此识别出状态变化。
    • ABA问题及其影响
      • ABA问题定义: 在CAS操作中,一个值A被线程修改成B后又改回A,但由于版本戳的变化,CAS操作能识别出这种变化,从而避免错误地认为值没有变化。
      • 影响分析:
        • 大多数情况下无影响: 对于简单的计数操作(如count++),中间值的变化(如从0变为100再变回1)通常不关心,只关注最终结果。
        • 特定场景有影响: 在需要精确控制状态变化的场景中,ABA问题可能导致错误判断,因此需要版本戳等机制来解决。
    • CAS解决ABA问题的价值
      • 价值体现: CAS操作结合版本戳,能有效解决ABA问题,确保在并发环境下数据的一致性和正确性。
      • 架构领域参考: 这种解决ABA问题的方法在架构设计中具有广泛的参考价值,特别是在需要高效并发控制的场景中。
    • 注意事项
      • 了解ABA问题: 虽然大多数情况下ABA问题对业务操作无影响,但开发者应了解并认识这一问题。
      • 适用场景判断: 在设计并发控制逻辑时,需根据具体场景判断是否需要考虑ABA问题及其解决方案。
  • CAS解决ABA问题 44:48
    • ABA问题概述
    • ABA问题的解决方法
    • CAS操作的其他问题
    • 并发环境下的特别注意
      • 特别注意ABA问题: 在并发环境下,由于多个线程可能同时操作同一个变量,因此需要特别注意ABA问题,确保数据的正确性和一致性。
      • 其他并发问题: 除了ABA问题外,还需要关注死锁、活锁、饥饿等其他并发问题,以确保系统的稳定性和性能。
  • 循环时间长与开销大 45:31
    • CAS操作与CPU消耗
      • CAS操作原理: CAS(Compare-And-Swap)是一种用于实现多线程同步的原子操作。它通过比较内存地址上的值与期望值,若相等则更新为新值,否则不做任何操作。
      • CPU消耗问题: 当大量线程竞争同一个资源时,CAS操作可能会频繁失败,导致线程不断重试(自旋),从而大量消耗CPU资源。
      • 与synchronized关键字对比: 在高度竞争的情况下,synchronized关键字的性能可能优于CAS。因为synchronized会使未获得锁的线程进入阻塞状态,不会消耗CPU资源;而CAS则会使线程空转,消耗大量CPU时间。
    • CAS与锁升级
      • 锁升级过程: synchronized关键字底层存在锁升级的过程,从偏向锁到轻量级锁,再到重量级锁。
      • 适用场景: 偏向锁和轻量级锁适用于竞争不激烈的情况;而重量级锁则在竞争激烈时使用。CAS操作在竞争激烈时,由于自旋消耗CPU,性能可能不如synchronized。
    • ABA问题
      • ABA问题定义: 当一个值从A变为B再变回A时,CAS操作无法检测到这种变化,但实际上值已经发生了变化。
      • 解决思路: 使用版本号(如AtomicStampedReference类),在变量前追加版本号,每次变量更新时版本号加1,从而解决ABA问题。
    • 只能保证一个共享变量的原子操作
      • 限制: CAS操作只能保证一个共享变量的原子性。当需要操作多个共享变量时,CAS无法保证整体操作的原子性。
      • 解决方案:
        • 将多个变量合并成一个对象,然后对该对象进行CAS操作。
        • 使用JDK提供的AtomicReference类,将多个变量放在一个对象中进行CAS操作。
    • LongAdder类
      • 引入背景: 在高并发情况下,AtomicInteger等原子操作类由于所有操作集中在某个变量上,可能导致写热点问题,成为性能瓶颈。
      • LongAdder原理: 通过分散写热点,将单一的热点变量分散到多个变量上,从而减少竞争,提高并发性能。
    • 数据分片与写热点分散
      • 数据分片: 将要处理的数据或请求分成多份,并行处理,是解决写热点问题的重要策略。
      • 应用实例:
        • 数据库的分库分表。
        • ES的分布式索引。
        • ConcurrentHashMap的分段锁。
  • LongAdder与写热点分散 01:00:42
    • LongAdder的设计思路
    • LongAdder的内部结构
    • base变量的作用
    • Cell数组的作用
    • LongAdder的add方法
    • LongAdder与AtomicInteger的区别
    • LongAdder的sum方法
      • sum方法: LongAdder的sum方法用于获取当前累加的总和。该方法会将base变量和所有Cell数组中的值进行汇总。但需要注意的是,由于并发操作的存在,sum方法返回的值可能是一个近似值,而不是精确值。
      • 强一致性: LongAdder不能保证强一致性,即它提供的值可能不是完全准确的。这是因为它在汇总过程中没有使用锁,从而允许其他线程在汇总过程中继续进行修改操作。因此,在使用LongAdder时,需要考虑到这一点。
  • 原子操作与事务的原子性 01:05:46
    • 原子操作的概念
    • CAS操作详解
      • CAS定义: CAS(Compare-And-Swap)是一种实现原子操作的方式,通过CPU指令支持,比较并交换内存中的值。
      • 操作过程: CAS操作包含三个运算符:一个内存地址V,一个期望的值A和一个新值B。如果地址V上的值等于期望的值A,则将地址V上的值赋为新值B;否则,不做任何操作。
      • 循环CAS: 由于CAS操作可能失败,因此在业务层面需要反复重试,直到成功为止,这就是循环CAS。
      • 问题: CAS操作可能引入ABA问题、循环时间长开销大以及只能保证一个共享变量的原子操作等问题。
    • Atomic系列原子操作类
      • 类介绍: Java中的Atomic系列原子操作类,如AtomicInteger、AtomicLong等,利用了循环CAS机制来实现原子操作。
      • 性能优化: 在JDK 1.8中,为了解决高竞争情况下Atomic性能下降的问题,引入了LongAdder等类,通过分散写热点的方式来提升性能。
    • 事务的原子性
      • 事务特性: 事务具有ACID四大特性,其中原子性(Atomicity)指事务中的多个操作要么全部执行,要么全都不执行。
      • 与原子操作的联系: 并发编程中的原子操作和事务的原子性具有相同的内涵和概念,都保证了操作的不可分割性。

三、使用场景

四、ABA问题

blog.csdn.net/qq_44300280…