少女祈祷中...
NEKO
知识库

java 锁面试要点

一、synchronized 与锁升级

Q1:synchronized 的三种使用方式是什么?

答:

// 1. 修饰实例方法:锁当前对象 this
public synchronized void method() {}

// 2. 修饰静态方法:锁当前 Class 对象
public static synchronized void staticMethod() {}

// 3. 修饰代码块:锁指定对象
public void block() {
    synchronized (lock) { }
}

本质:synchronized 锁的是对象,不是代码。


Q2:synchronized 的底层原理是什么?

答: 通过 monitorenter / monitorexit 字节码指令实现:

synchronized (lock) {
    // 代码
}

编译后:

monitorenter
// 代码
monitorexit
  • 每个对象关联一个 Monitor(管程)

  • monitorenter:尝试获取 Monitor 所有权

  • monitorexit:释放 Monitor

  • 异常时,编译器会自动插入 monitorexit(保证释放)

方法级 synchronized 通过方法访问标志 ACC_SYNCHRONIZED 实现,原理相同。


Q3:什么是 Monitor?它的结构是什么?

答: Monitor 是 JVM 实现的同步机制,对应 C++ 的 ObjectMonitor:

字段

含义

_owner

持有锁的线程

_count

重入次数

_EntryList

等待锁的线程队列

_WaitSet

调用 wait() 的线程队列

_recursions

重入次数

工作流程:

  1. 线程进入 _EntryList 竞争锁

  2. 获取成功,_owner 指向自己,_count++

  3. 调用 wait(),进入 _WaitSet

  4. 释放锁,_owner = null,唤醒 _EntryList 线程


Q4:synchronized 的锁升级过程是什么?

答: 锁状态从低到高:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

升级过程:

  1. 偏向锁:只有一个线程访问,Mark Word 记录线程 ID,无竞争

  2. 轻量级锁:有第二个线程竞争,升级为轻量级锁,用 CAS 自旋

  3. 重量级锁:自旋失败或竞争激烈,升级为重量级锁,线程阻塞

关键:锁升级是单向的,不可降级(HotSpot 中偏向锁可批量撤销,但整体趋势单向)。


Q5:什么是偏向锁?为什么需要它?

答: 偏向锁针对只有一个线程访问同步块的场景优化:

  • 第一次获取锁时,把线程 ID 写入对象头的 Mark Word

  • 后续同一线程进入,只需检查线程 ID,无需 CAS

  • 几乎无开销

// 对象头 Mark Word(64 位)
// 偏向锁:| 线程ID(54) | Epoch(2) | 未使用(1) | 分代年龄(4) | 偏向标志(1) | 锁标志(2) |

优点:单线程场景下,避免 CAS 开销。

注意:JDK 15 后偏向锁默认禁用(维护成本高,收益有限)。


Q6:什么是轻量级锁?它的原理是什么?

答: 轻量级锁针对少量线程竞争、锁持有时间短的场景:

  1. 线程在栈帧中创建 Lock Record

  2. 用 CAS 把对象头的 Mark Word 替换为指向 Lock Record 的指针

  3. 成功则获取锁;失败则自旋重试

  4. 自旋失败,升级为重量级锁

// 轻量级锁 Mark Word
// | 指向栈中锁记录的指针(62) | 锁标志(2) = 00 |

优点:避免线程阻塞和上下文切换。

缺点:自旋消耗 CPU,竞争激烈时反而更慢。


Q7:什么是自旋锁?自适应自旋是什么?

答:

  • 自旋锁:不立即阻塞,而是循环等待(CAS),避免上下文切换

  • 自适应自旋:JVM 根据历史成功率动态调整自旋次数

// 伪代码
while (!cas(lock, unlocked, locked)) {
    // 自旋
}

自适应策略:

  • 上次自旋成功 → 允许更多自旋

  • 上次自旋失败 → 减少甚至跳过自旋

JDK 6 后默认开启自适应自旋。


Q8:什么是重量级锁?

答: 竞争激烈时,锁升级为重量级锁:

  • 依赖操作系统 Mutex Lock

  • 未获取锁的线程阻塞,进入 _EntryList

  • 涉及用户态 → 内核态切换,开销大

特点:

  • 不会空转 CPU

  • 但上下文切换成本高

适用:锁持有时间长、竞争激烈的场景。


Q9:锁升级的优缺点是什么?

答:

锁状态

优点

缺点

适用场景

偏向锁

几乎无开销

有竞争时需撤销

单线程

轻量级锁

避免阻塞

自旋耗 CPU

少量竞争、短临界区

重量级锁

不空转 CPU

上下文切换慢

激烈竞争、长临界区

核心思想:根据竞争程度,逐级升级,平衡开销。


Q10:synchronized 和 ReentrantLock 的区别?

答:

对比项

synchronized

ReentrantLock

实现

JVM 层(Monitor)

JDK 层(AQS)

锁释放

自动

手动 unlock

可中断

不可

可(lockInterruptibly)

超时

不可

可(tryLock)

公平锁

非公平

可选公平/非公平

条件队列

1 个

多个 Condition

性能

JDK 6 后优化好

相近

使用

简单

灵活但易错

选择:简单场景用 synchronized,需要高级功能用 ReentrantLock。


二、ReentrantLock

Q11:什么是可重入锁?ReentrantLock 如何实现可重入?

答: 可重入锁指同一线程可以多次获取同一把锁:

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock(); // 可重入
lock.unlock();
lock.unlock();

实现:

  • AQS 的 state 字段记录重入次数

  • 每次 lock():state++

  • 每次 unlock():state--,减到 0 才释放

// ReentrantLock 非公平锁
final boolean nonfairTryAcquire(int acquires) {
    if (state == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(state + acquires); // 重入
        return true;
    }
    return false;
}

Q12:ReentrantLock 的公平锁和非公平锁有什么区别?

答:

对比项

公平锁

非公平锁

获取顺序

FIFO,先到先得

允许插队

吞吐量

低

高

饥饿

不会

可能

默认

否

是

ReentrantLock fair = new ReentrantLock(true);   // 公平
ReentrantLock unfair = new ReentrantLock(false); // 非公平(默认)

非公平锁:新线程直接 CAS 抢锁,抢不到才排队,减少唤醒开销。


Q13:ReentrantLock 的 tryLock() 有什么用?

答: 尝试获取锁,不阻塞:

ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { // 立即返回
    try { /* 业务 */ }
    finally { lock.unlock(); }
} else {
    // 获取失败,做其他事
}

// 带超时
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try { /* 业务 */ }
    finally { lock.unlock(); }
}

用途:避免死锁、定时任务、降级处理。


Q14:lockInterruptibly() 和 lock() 有什么区别?

答:

  • lock():不可中断,一直等待

  • lockInterruptibly():可被 interrupt() 中断

lock.lockInterruptibly();
try {
    // 若被中断,抛 InterruptedException
} finally {
    lock.unlock();
}

用途:避免线程无限等待,支持取消操作。


Q15:ReentrantLock 必须放在 try-finally 中吗?

答: 必须,否则异常时锁不释放,导致死锁:

// 错误
lock.lock();
doSomething(); // 抛异常,锁永远不释放
lock.unlock();

// 正确
lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

注意:lock() 应在 try 之前,避免 lock 失败后 unlock。


三、AQS 原理

Q16:什么是 AQS?

答: AQS(AbstractQueuedSynchronizer)是 JDK 并发包的基础框架:

  • 核心:一个 volatile int state + CLH 双向队列

  • 子类只需实现 tryAcquire / tryRelease 等方法

  • 基于 AQS 的组件:ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock

public abstract class AbstractQueuedSynchronizer {
    private volatile int state;          // 同步状态
    private transient volatile Node head; // 队列头
    private transient volatile Node tail; // 队列尾
}

Q17:AQS 的核心结构是什么?

答:

  1. state:volatile int,表示同步状态

    • ReentrantLock:0 未锁,>0 重入次数

    • Semaphore:剩余许可数

    • CountDownLatch:剩余计数

  2. CLH 队列:双向链表,存放等待线程

    • 每个节点是 Node,包含线程引用和等待状态

    • FIFO 顺序

  3. Condition 队列:条件等待队列

head → node1 → node2 → ... → tail

Q18:AQS 的独占模式和共享模式有什么区别?

答:

模式

方法

代表

独占

tryAcquire / tryRelease

ReentrantLock

共享

tryAcquireShared / tryReleaseShared

Semaphore、CountDownLatch

  • 独占:同一时刻只有一个线程持有

  • 共享:多个线程可同时持有

// 独占:state 0→1
// 共享:state 表示可用许可数,可多个线程同时减

Q19:AQS 的 acquire 流程是什么?

答: 以 ReentrantLock 为例:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&           // 1. 尝试获取
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { 
        // 2. 失败则入队,自旋/阻塞
        selfInterrupt();
    }
}

流程:

  1. tryAcquire:尝试 CAS 修改 state

  2. 失败:创建 Node,加入 CLH 队列尾部

  3. acquireQueued:自旋检查前驱是否为 head,是则尝试获取

  4. 仍失败:LockSupport.park() 阻塞

  5. 前驱释放锁时 unpark 唤醒


Q20:AQS 的 release 流程是什么?

答:

public final boolean release(int arg) {
    if (tryRelease(arg)) {              // 1. 尝试释放
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);          // 2. 唤醒后继节点
        return true;
    }
    return false;
}

流程:

  1. tryRelease:state 减到 0,设置 owner 为 null

  2. 唤醒 head 的后继节点

  3. 被唤醒线程重新竞争锁


Q21:AQS 中为什么要用自旋 + 阻塞?

答:

  • 纯自旋:CPU 空转,浪费资源

  • 纯阻塞:上下文切换开销大

AQS 策略:

  1. 先自旋几次,尝试获取锁(快速路径)

  2. 失败则 park() 阻塞(慢速路径)

  3. 被唤醒后再自旋竞争

平衡:短等待用自旋,长等待用阻塞。


Q22:AQS 的 Node 有哪些状态?

答: waitStatus:

状态

值

含义

CANCELLED

1

已取消

SIGNAL

-1

后继节点需要唤醒

CONDITION

-2

在 Condition 队列中

PROPAGATE

-3

共享模式传播

0

0

初始状态

SIGNAL:当前节点释放锁时,必须唤醒后继节点。


Q23:为什么 AQS 用双向队列而不是单向?

答: 因为需要从尾部找有效前驱:

  • 节点取消时,需要向前查找

  • unparkSuccessor 从尾部往前找,避免遍历整个队列

  • 双向队列支持 O(1) 尾部插入和高效前向查找

// 从尾部往前找有效节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev)
        if (t.waitStatus <= 0) s = t;
}

Q24:AQS 如何实现公平锁和非公平锁?

答:

非公平锁:直接 CAS 抢,不看队列

final boolean nonfairTryAcquire(int acquires) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
    // ...
}

公平锁:先检查是否有前驱

protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && !hasQueuedPredecessors() 
        && compareAndSetState(0, acquires)) {
        // 只有队列为空才抢
    }
}

区别:公平锁多一个 hasQueuedPredecessors() 检查。


四、读写锁

Q25:什么是读写锁?解决什么问题?

答: ReentrantReadWriteLock 分离读锁和写锁:

  • 读锁(共享):多个线程可同时读

  • 写锁(独占):只有一个线程可写

  • 读写互斥、写写互斥、读读不互斥

解决问题:读多写少场景下,synchronized 让读也串行,性能差。

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();
WriteLock writeLock = rwLock.writeLock();

readLock.lock();   // 共享
readLock.unlock();

writeLock.lock();  // 独占
writeLock.unlock();

Q26:读写锁的规则是什么?

答:

组合

是否允许

读 + 读

✅ 允许

读 + 写

❌ 互斥

写 + 写

❌ 互斥

写 + 读

❌ 互斥

总结:读读共享,其他互斥。


Q27:读写锁如何用 AQS 实现?

答: 用 state 的高 16 位表示读锁,低 16 位表示写锁:

// state 高16位:读锁持有数
// state 低16位:写锁重入次数
static final int SHARED_SHIFT   = 16;
static final int EXCLUSIVE_MASK = (1 << 16) - 1;

// 读锁数量
static int sharedCount(int c) { 
    return c >>> SHARED_SHIFT; 
}
// 写锁数量
static int exclusiveCount(int c) { 
    return c & EXCLUSIVE_MASK; 
}

读锁:tryAcquireShared,CAS 增加高 16 位
写锁:tryAcquire,CAS 增加低 16 位


Q28:读写锁支持锁降级吗?什么是锁降级?

答: 支持写锁降级为读锁,不支持读锁升级为写锁。

// 锁降级
writeLock.lock();
try {
    // 修改数据
    readLock.lock();  // 获取读锁
} finally {
    writeLock.unlock(); // 释放写锁,仍持有读锁
}
try {
    // 读数据
} finally {
    readLock.unlock();
}

为什么支持降级:保证数据可见性和一致性。

为什么不支持升级:多个读锁同时升级会死锁。


Q29:什么是锁饥饿?读写锁如何避免写饥饿?

答: 读多写少时,读锁可能一直占用,写锁永远等不到 → 写饥饿。

ReentrantReadWriteLock 策略:

  • 非公平模式:读锁可插队,可能写饥饿

  • 公平模式:FIFO,避免饥饿

  • 写锁请求会阻塞后续读锁(非公平下也有一定保护)

ReentrantReadWriteLock fair = 
    new ReentrantReadWriteLock(true);

Q30:StampedLock 是什么?和读写锁有什么区别?

答: StampedLock 是 JDK 8 引入的乐观读锁:

对比项

ReentrantReadWriteLock

StampedLock

悲观读

支持

支持

乐观读

不支持

支持

可重入

是

否

锁升级

不支持

支持

性能

一般

更高

StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 乐观读
int data = readData();
if (!lock.validate(stamp)) { // 检查是否被修改
    stamp = lock.readLock(); // 降级为悲观读
    try { data = readData(); }
    finally { lock.unlockRead(stamp); }
}

乐观读:不加锁,读完检查版本号,适合读多写极少。


五、CAS 与原子类

Q31:什么是 CAS?

答: CAS(Compare And Swap)是一种无锁原子操作:

// 伪代码
boolean cas(int expected, int newValue) {
    if (value == expected) {
        value = newValue;
        return true;
    }
    return false;
}

三个操作数:

  • V:内存值

  • A:预期值

  • B:新值

当且仅当 V == A 时,V = B,否则不做任何操作。

底层:CPU 的 cmpxchg 指令,保证原子性。


Q32:CAS 有什么问题?

答: 三大问题:

  1. ABA 问题

    • 值从 A → B → A,CAS 无法感知

    • 解决:加版本号(AtomicStampedReference)

  2. 自旋开销

    • 竞争激烈时,长时间自旋耗 CPU

    • 解决:自适应自旋、限制自旋次数

  3. 只能保证一个变量原子性

    • 多个变量需用锁或 AtomicReference

// ABA 问题示例
AtomicInteger ai = new AtomicInteger(1);
// 线程1:读到1,准备 CAS 为 2
// 线程2:1→2→1
// 线程1:CAS(1, 2) 成功,但中间已变过

Q33:如何解决 ABA 问题?

答: 用版本号:

AtomicStampedReference<Integer> ref = 
    new AtomicStampedReference<>(1, 0);

int stamp = ref.getStamp();
ref.compareAndSet(1, 2, stamp, stamp + 1);

原理:每次修改版本号 +1,CAS 同时比较值和版本号。

AtomicMarkableReference 用 boolean 标记,类似。


Q34:原子类有哪些?分类是什么?

答: JDK 提供 java.util.concurrent.atomic 包:

分类

类

基本类型

AtomicInteger、AtomicLong、AtomicBoolean

数组

AtomicIntegerArray、AtomicLongArray

引用

AtomicReference、AtomicStampedReference、AtomicMarkableReference

字段更新

AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater

累加器(JDK 8)

LongAdder、DoubleAdder、LongAccumulator


Q35:AtomicInteger 的底层原理是什么?

答: 基于 CAS + volatile:

public class AtomicInteger {
    private volatile int value;
    
    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }
    
    // Unsafe 方法
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
}
  • value 用 volatile 保证可见性

  • Unsafe.compareAndSwapInt 保证原子性

  • 失败则自旋重试


Q36:LongAdder 和 AtomicLong 有什么区别?

答:

对比项

AtomicLong

LongAdder

原理

单个 CAS

分段 CAS(Cell 数组)

高并发

竞争激烈,性能差

分散竞争,性能好

读取

精确

可能不精确(求和)

适用

低并发、需精确

高并发统计

LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum(); // 累加所有 Cell

LongAdder 核心:把热点分散到多个 Cell,减少 CAS 冲突。


Q37:LongAdder 的分段思想是什么?

答:

  • 维护一个 base 变量 + Cell[] 数组

  • 无竞争:直接 CAS base

  • 有竞争:哈希到某个 Cell,CAS 该 Cell

  • 求和:base + 所有 Cell 的值

// 伪代码
public void increment() {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || 
        !casBase(b = base, b + 1)) {
        // 竞争激烈,使用 Cell
        int index = getProbe() & (as.length - 1);
        casCell(as[index], 1);
    }
}

思想:空间换时间,分散热点。


Q38:CAS 和 synchronized 如何选择?

答:

场景

推荐

简单计数

AtomicInteger

高并发计数

LongAdder

复杂临界区

synchronized / Lock

多变量原子操作

synchronized / AtomicReference

低竞争

CAS 性能更好

高竞争

锁更稳定

核心:CAS 适合简单、低竞争;锁适合复杂、高竞争。


Q39:什么是乐观锁和悲观锁?

答:

对比项

乐观锁

悲观锁

思想

假设不冲突,冲突再重试

假设会冲突,先加锁

实现

CAS、版本号

synchronized、Lock

适用

读多写少

写多读少

开销

冲突时重试

加锁开销

死锁

不会

可能

乐观锁:CAS、数据库 version 字段
悲观锁:synchronized、SELECT ... FOR UPDATE


Q40:AtomicReference 如何使用?

答:

AtomicReference<String> ref = new AtomicReference<>("A");

// CAS 更新
boolean success = ref.compareAndSet("A", "B");

// 获取并更新
String old = ref.getAndSet("C");

// 函数式更新
ref.updateAndGet(s -> s + "!");

用途:原子更新对象引用,实现无锁数据结构。


六、综合与进阶

Q41:synchronized 锁升级和 AQS 有什么关系?

答: 两者是不同的锁实现:

  • synchronized:JVM 层,Monitor + 锁升级

  • AQS:JDK 层,state + CLH 队列

联系:

  • 都基于 CAS

  • 都涉及自旋和阻塞

  • synchronized 由 JVM 优化,AQS 由开发者控制

选择:简单用 synchronized,复杂用 AQS 系(ReentrantLock 等)。


Q42:ReentrantLock 的 lock() 和 lockInterruptibly() 底层区别?

答:

  • lock():acquire(1),不可中断

  • lockInterruptibly():acquireInterruptibly(1),可中断

// 可中断版本
public final void acquireInterruptibly(int arg) {
    if (Thread.interrupted()) throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg); // 阻塞时检查中断
}

区别:可中断版本在 park 时响应中断,抛异常。


Q43:AQS 的 Condition 如何实现?

答: Condition 内部维护等待队列:

  • await():释放锁,加入等待队列,park

  • signal():把等待队列头节点移到同步队列

  • 被唤醒后重新竞争锁

await: 同步队列 → 等待队列
signal: 等待队列 → 同步队列

核心:两个队列之间的节点转移。


Q44:如何实现一个自定义同步器?

答: 继承 AQS,实现 tryAcquire/tryRelease:

class Mutex extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int arg) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(
                Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int arg) {
        if (getState() == 0) 
            throw new IllegalMonitorStateException();
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()   { acquire(1); }
    public void unlock() { release(1); }
}

要点:只需实现 tryAcquire/tryRelease,其余由 AQS 处理。


Q45:为什么 AQS 的 state 用 volatile?

答: 保证可见性:

  • 一个线程修改 state,其他线程立即可见

  • 配合 CAS 保证原子性

  • volatile + CAS = 无锁并发的基石

private volatile int state;

注意:volatile 只保证可见性,原子性由 CAS 保证。

评论