CAS和AQS相关问题

CAS

CAS(M,A,B)其实是由Unsafe类提供的一个cpu指令,在无锁状态下保证原子性,体现了一种乐观锁思想

如果M和A的值相同,就把M和B的值进行交换,交换的本质是为了把B的值赋给A,

而不关心B值具体是什么。如果失败则会自旋重试,直到成功为止

缺点:ABA问题,自旋开销,不推荐直接使用,只能适合一些特定场景,例如AQS框架,也可以用封装好的例如AtomicLong、AtomicReference

CAS总线风暴

总线风暴 是指在高并发场景下,大量线程频繁使用 CAS 操作,导致系统总线被反复锁定,释放,形成风暴,从而引起整体性能急剧下降的现象。

解决方案: 1. LongAdder :空间换时间,分段 CAS

  1. 批量操作 :减少 CAS 调用次数

  2. 退避策略:避免同时争抢

ABA问题

CAS进行操作的关键是通过值没有发生变化来作为"没有其他线程穿插执行"判定依据

但这种问题不够严谨,近端情况下,有另一个线程穿插进来,把值从A->B->A

ABA问题如果真的出现了,其实大部分情况下也不会产生bug,虽然另一个线程穿插执行,由于值又改回去了,此时逻辑上也不一定会产生bug

只要让判定的数值按照一个方向增长即可,有增有减就有可能出现ABA

但是针对账户余额这样的概念,本身就应该要能增能减,可以引入一个额外的变量"版本号"

它有两种类型,一种为int记录被修改的次数,另一种为boolean仅表示是否被修改过

提问:除了版本号,还有什么办法解决 ABA 问题?

回答:几个思路:第一是用锁,直接放弃 CAS,简单粗暴但失去了无锁的性能优势。第二是对象不复用,每次都 new 新对象,靠引用地址不同来规避 ABA,但会增加 GC 压力。第三是延迟回收,像 Hazard Pointer 或者 RCU 这种技术,保证在有线程持有引用期间不会真正释放内存,从根源上避免地址复用。Linux 内核的无锁数据结构就大量使用 RCU。

谈谈你对AQS的理解

AQS是Java并发包里的一个抽象同步框架,核心作用就是统一分装了线程的等待,唤醒,排队机制

底层通过state变量+FIFO的队列来实现类线程安全的资源抢夺

state是通过volatile类型变量表示同步状态,在不同的同步器扮演不同角色,在ReentrantLock中就是锁的持有状态,Semaphore就是剩余的许可证数量,而CountDownLatch就是还需等待的倒数次数

线程要想获取资源先CAS改state,成功的话就拿到资源,失败了就被挂到队列里面排队,而这个队列是变体的CLH队列,用双向链表实现

AQS不负责加不加锁,它只是帮忙处理排队的细节,真正的锁逻辑交给实现类自己决定

它有两种模式,一种是独占模式,意味着同时只有一个线程能持有资源,典型实现为ReentrantLock

另一种为共享模式,多个线程可以同时持有资源

提问:AQS 为什么用 CLH 队列的变体而不是普通队列?

回答:原版 CLH 是自旋等待前驱节点释放锁,每个线程只需要关注自己前驱的状态,避免了所有线程 CAS 同一个变量导致的总线风暴。AQS 把自旋改成了 park 阻塞,这样长时间等锁不会白白烧 CPU。但阻塞了就没法自己检测前驱状态,所以改成双向链表,让前驱主动 unpark 唤醒后继。

Reentrantlock

Reentrantlock也是一个重入锁,使用上和synchronized类似,但是需要手动解锁和释放锁

底层是AQS的独占模式

优势:

  1. 加锁时有两种方式lock和trylock
  2. 提供了公平锁的实现
  3. 提供了更强大的等待通知,搭配了Condition类,实现等待通知的
    两者区别

Semaphore(信号量)

它是一个同步工具类用于限制重视访问特定资源的限制数量

线程调用acquire(),如果许可数大于0,计数器-1,线程继续执行,如果许可数等于0,则线程则会阻塞等待。线程任务完成则会调用release(),计数器+1,同时唤醒一个等待的线程

它的底层依赖AQS,许可证就是AQS的state值。

开发中如果遇到需要申请资源的场景,就可以使用Semaphore来实现了

主要有两种工作模式,公平和非公平

CountDownLatch

这个东西主要适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成

比如需要把一个大的任务,拆成多个小的任务,让这些任务并发的去执行

就可以使用使用countDownLatch来判定这些任务是否全都完成

主要有两个方法

  1. 主线程调用await()的时候就会阻塞,就会等待其他线程完成任务,所有线程都完成了任务以后,此时这个await才会继续往下走
  2. countDown()告诉countDownLatch,我当前这个子任务已经完成了
    CountDownLatch底层基于AQS的共享模式实现,计数器值就是AQS的state,但是这个state只能向下减,减到0后不能重置,因此它不可复用,是一次性的

CyclicBarrier

CyclicBarrier是一个循环屏障,允许一组线程互相的等待,直到所有线程到达屏障点后再一起继续执行,可以重复使用,适合多轮同步的场景。底层基于ReentrantLock + Condition实现,内部有一个count计数器和一个generation代,线程调用await()时获取锁,然后计数器-1,计数器不为0则会阻塞当前线程,如果减为0,说明最后一个线程到达,先执行barrierAction进行回调,再执行condition.signalALL()唤醒所有线程,然后重置计数器,更新generation开启下一轮。

常见使用场景:游戏匹配系统,所有玩家都点击准备后才开始游戏。压力测试,所有线程准备就绪后同时发起请求,模拟并发峰值。

当等待过程中有线程被中断或者超时,屏障会被打破,其他线程会收到BrokenBarrierException。这时候需要调用 reset() 重置屏障。

提问:CountDownLatch 和 CyclicBarrier 都能实现线程等待,有什么区别?

回答:CountDownLatch 是一次性的,计数减到 0 就没法复用了,主线程等其他线程完成任务用这个。CyclicBarrier 可以重复使用,所有线程到达屏障点后一起放行,然后计数重置,适合多轮并行计算的场景。底层实现也不一样,CountDownLatch 基于 AQS 的共享模式,CyclicBarrier 基于 ReentrantLock + Condition。

CopyOnWriteArrayList写实拷贝

比如两个线程使用同一份ArrayList,如果两个线程读就直接读

如果某个线程需要修改,就会把ArratList复制出一份副本,修改线程的话就修改这个副本,

与此同时另一个线程仍然可以读取数据,一旦修改完成就会使用修改好的这份数据,替代掉原来的数据

缺点:这个ArrayList不能太大(拷贝成本高),更适合多个线程读,一个线程去修改

应用场景:服务器的配置更新

相关推荐
Seven971 小时前
剑指offer-78、求平⽅根
java
玄〤1 小时前
个人博客网站搭建day6--Spring Boot自定义RedisTemplate配置:优化序列化与Java8时间类型支持
java·spring boot·redis·后端·spring
知我Deja_Vu2 小时前
@Transactional 与 @Transactional(rollbackFor = Exception.class) 的区别详解
java·spring
上海合宙LuatOS2 小时前
LuatOS核心库API——【iotauth 】 IOT 鉴权库
java·单片机·嵌入式硬件·物联网·struts·计算机外设·硬件工程
luod2 小时前
Docker 快速安装Jenkins
java·docker·jenkins
senijusene2 小时前
Linux软件编程: 线程属性与线程间通信详解
java·linux·jvm·算法
昱宸星光2 小时前
spring cloud gateway内置路由断言工厂
java·开发语言·前端
亓才孓2 小时前
jdk动态代理和Cglib动态代理的区别,为什么Cglib更适配SpringAOP
java·开发语言
塔中妖2 小时前
Windows 安装 Maven 详细教程(含镜像与本地仓库配置)
java·windows·maven