2.4 CAS 知道吗?
CAS 就是 Compare And Swap,比较并交换,它体现的是乐观锁思想。
它会比较当前内存值和期望值,如果相等,就更新成新值;如果不相等,说明被其他线程改过,就更新失败,通常会自旋重试。
Java 里的 Atomic 类和 AQS 都大量用了 CAS。它底层依赖 CPU 原子指令保证比较和更新的原子性。CAS 的优点是低竞争下避免加锁阻塞,但缺点是高竞争会自旋浪费 CPU,也有 ABA 问题。
2.5 请谈谈你对 volatile 的理解
volatile 我理解它主要解决两个问题:一个是可见性,一个是禁止指令重排序。
可见性就是说,一个线程修改了 volatile 变量之后,其他线程后续读取这个变量时,能够读到最新值,不会一直读自己线程里的旧缓存。禁止重排序主要是通过内存屏障实现的,保证 volatile 读写前后的指令不会被随意调整,比如双重检查锁里就需要 volatile 防止对象还没初始化完就被其他线程拿到。
但 volatile 有个重要限制,它不保证复合操作的原子性。比如
volatile int count,执行count++还是线程不安全,因为++包含读取、加一、写回三步,多个线程并发时还是会丢失更新。所以 volatile 更适合做状态标记,或者配合一些特定场景使用,不能替代 synchronized 或 Atomic 类。
2.6 什么是AQS?
AQS 是
AbstractQueuedSynchronizer,可以理解成 JUC 里面很多锁和同步器的基础框架,比如 ReentrantLock、Semaphore、CountDownLatch 底层都跟它有关。它的核心主要是两部分:一个是
state,一个是 FIFO 等待队列。state是一个同步状态,具体含义由不同工具自己定义。比如 ReentrantLock (阻塞式锁)里,state表示锁的重入次数;Semaphore 里表示剩余许可数量;CountDownLatch (倒计时锁)里表示倒计数。修改state时通常会用 CAS 来保证线程安全。大概流程是:线程先尝试获取
state,如果获取成功就继续执行;如果失败,就会被封装成节点放到 AQS 的等待队列里阻塞。等持有资源的线程释放之后,再唤醒队列里的后继线程继续竞争。所以我理解 AQS 的核心就是:用 state 管资源状态,用 CAS 改状态,用 FIFO 队列管理等待线程。
2.7 ReentrantLock的实现原理
ReentrantLock 是一个可重入互斥锁,使用上是通过
lock()获取锁,unlock()释放锁。它底层主要是基于 AQS 实现的。在 ReentrantLock 里,AQS 的
state表示锁的重入次数。state=0表示锁还没人持有,线程加锁时会通过 CAS 尝试把state从 0 改成 1,成功就说明拿到了锁,同时会记录当前持锁线程。如果同一个线程再次调用lock(),不会阻塞,而是直接让state++,这就是可重入。如果其他线程来抢锁失败,就会被封装成节点放进 AQS 的 FIFO 等待队列里阻塞。释放锁时,每次
unlock()会让state--,只有减到 0 才算真正释放锁,然后会唤醒队列里的后继线程继续竞争。它还支持公平锁和非公平锁,默认是非公平锁。非公平锁线程来了会先尝试直接抢锁,可能插队;公平锁会先看队列里有没有线程在等,有的话就排队,所以公平锁更公平,但性能通常差一点。
2.8 synchronized和Lock有什么区别 ?
synchronized 和 Lock 都可以实现互斥和可重入,但它们最大的区别是使用方式和功能灵活性不一样。
synchronized 是 Java 关键字,由 JVM 管理锁的获取和释放。进入同步代码块时自动加锁,退出代码块或者发生异常时也会自动释放,所以用起来比较简单,不容易忘记释放锁。
Lock 是 JUC 里的接口,常见实现是 ReentrantLock。它需要手动调用
lock()和unlock(),所以一般要把unlock()放到finally里,避免异常导致锁不释放。它的优势是功能更灵活,比如可以尝试获取锁tryLock(),可以超时等待,可以响应中断,也可以选择公平锁,还支持多个 Condition 条件队列。性能上现在不能简单说谁一定更快。synchronized 经过 JVM 优化后性能已经不差。一般简单同步场景我会优先用 synchronized;如果需要可中断、超时、公平锁或者多个条件队列,就用 ReentrantLock 这类 Lock 实现。