CAS
CAS(M,A,B)其实是由Unsafe类提供的一个cpu指令,在无锁状态下保证原子性,体现了一种乐观锁思想
如果M和A的值相同,就把M和B的值进行交换,交换的本质是为了把B的值赋给A,
而不关心B值具体是什么。如果失败则会自旋重试,直到成功为止
缺点:ABA问题,自旋开销,不推荐直接使用,只能适合一些特定场景,例如AQS框架,也可以用封装好的例如AtomicLong、AtomicReference
CAS总线风暴
总线风暴 是指在高并发场景下,大量线程频繁使用 CAS 操作,导致系统总线被反复锁定,释放,形成风暴,从而引起整体性能急剧下降的现象。
解决方案: 1. LongAdder :空间换时间,分段 CAS
-
批量操作 :减少 CAS 调用次数
-
退避策略:避免同时争抢
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的独占模式
优势:
- 加锁时有两种方式lock和trylock
- 提供了公平锁的实现
- 提供了更强大的等待通知,搭配了Condition类,实现等待通知的
两者区别
Semaphore(信号量)
它是一个同步工具类用于限制重视访问特定资源的限制数量
线程调用acquire(),如果许可数大于0,计数器-1,线程继续执行,如果许可数等于0,则线程则会阻塞等待。线程任务完成则会调用release(),计数器+1,同时唤醒一个等待的线程
它的底层依赖AQS,许可证就是AQS的state值。
开发中如果遇到需要申请资源的场景,就可以使用Semaphore来实现了

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

CountDownLatch
这个东西主要适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成
比如需要把一个大的任务,拆成多个小的任务,让这些任务并发的去执行
就可以使用使用countDownLatch来判定这些任务是否全都完成
主要有两个方法
- 主线程调用await()的时候就会阻塞,就会等待其他线程完成任务,所有线程都完成了任务以后,此时这个await才会继续往下走
- 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不能太大(拷贝成本高),更适合多个线程读,一个线程去修改
应用场景:服务器的配置更新