真实面经题目 · 原创解析

线程锁锁的到底是什么?

线程锁并不是把某个线程本身锁住,也不是直接把一段代码或一个变量物理锁住。更准确地说,锁是一种同步协议:它通过某个可竞争的同步状态,约束多个线程进入临界区的顺序,从而保护共享资源及其不变量。Java 中 synchronized 竞争的是对象监视器或类对象监视器,ReentrantLock 竞争的是基于 AQS 维护的同步状态;操作系统层面的 mutex、semaphore 等竞争的是内核或用户态维护的同步状态。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

可以这样回答:线程锁锁的不是线程本身,而是某个同步状态;它真正保护的是临界区里共享资源的一致性。比如 Java 的 synchronized 加在普通方法或代码块上时,竞争的是某个对象的 monitor;加在静态方法或类对象上时,竞争的是 Class 对象对应的 monitor。ReentrantLock 则不是对象监视器,而是通过 AQS 的 state 和等待队列来表达是否被占用、被谁持有、是否可重入。再往下看,操作系统里的互斥量、信号量也不是锁住资源实体,而是锁住一份同步元数据,让线程根据这份状态阻塞、唤醒或继续执行。所以说锁锁住资源只是简化说法,严谨表达应该是:锁通过同步状态限制并发访问,保护共享资源的不变量;只有所有相关读写路径都使用同一把锁,这个保护才成立。

考点 锁约束进入临界区的资格
主线 锁保护共享资源的不变量
易错点 误以为锁是把线程锁住。线程只是因为竞争失败而阻塞或等待…

深入解析

01

锁约束进入临界区的资格

线程锁容易被误解为把线程关起来。实际上线程是执行单元,锁影响的是线程能不能继续进入某段受保护的逻辑。当一个线程拿不到锁时,它可能自旋、阻塞、挂起或进入等待队列;这只是调度结果,不代表锁的对象是线程。锁的核心语义是:在同一时刻,只允许满足条件的线程进入临界区执行。

02

锁保护共享资源的不变量

临界区通常会读写共享变量、集合、缓存、连接状态、余额、库存、任务队列等。真正需要保护的不是某一行代码,而是这些共享资源之间必须始终成立的关系,也就是不变量。例如数量不能为负、map 与 list 必须同步更新、状态从未开始到进行中再到完成不能倒退。锁的价值是让一组操作看起来像一个不可被打断的整体。

03

synchronized 锁的是监视器

在 Java 里,synchronized 的锁粒度取决于它绑定的对象。普通同步方法默认使用当前实例对象的 monitor;同步代码块使用括号里指定对象的 monitor;静态同步方法使用对应 Class 对象的 monitor。这里的锁不是方法本身,也不是变量本身,而是某个对象关联的监视器。只要两个 synchronized 片段使用的是同一个监视器,它们就会互斥。

04

ReentrantLock 锁的是 AQS 同步状态

ReentrantLock 的实现思路不同于 synchronized。它通过 AQS 维护一个 state 字段表示锁的占用次数,通过 owner 记录当前持有线程,并通过等待队列管理获取失败的线程。可重入的含义是同一线程再次获取锁时 state 递增,释放时递减,直到归零才真正释放。这里被竞争的本质是同步器内部的状态。

05

操作系统锁竞争同步原语状态

在更底层,mutex、semaphore、读写锁、条件变量等同步原语也不会直接把某个资源实体锁住。它们维护的是计数、拥有者、等待队列、唤醒关系等同步状态。线程根据这些状态决定继续运行、进入内核阻塞、在用户态自旋,或被其他线程唤醒。不同实现可能有用户态快路径和内核态慢路径,但原则相同。

06

同一把锁才有保护意义

锁的正确性依赖约定。假设一个共享对象有两条访问路径,一条加 lockA,另一条加 lockB,那么这两条路径之间并不互斥;如果还有一条路径完全不加锁,锁也无法阻止它读到中间态或写坏数据。因此判断锁是否有效,不能只看有没有加锁,而要看所有读写同一组共享状态的地方是否统一使用同一把锁。

07

锁还承担可见性与有序性语义

在 Java 内存模型中,锁不仅提供互斥,还提供内存可见性。释放锁之前的写入,对随后获取同一把锁的线程可见。也就是说,同一把锁建立了 happens-before 关系,使线程不会只看到过期值或乱序导致的不一致状态。没有这层语义,即使线程没有同时执行临界区,也可能因为缓存和重排序看到错误结果。

易错点

  • 误以为锁是把线程锁住。线程只是因为竞争失败而阻塞或等待,锁真正竞争的是同步状态。
  • 误以为锁住的是变量本身。变量不会知道自己被锁,只有所有访问方都通过同一把锁访问,保护才成立。
  • 误以为 synchronized 方法之间一定互斥。是否互斥取决于绑定的对象监视器是否相同。
  • 误以为加了锁就一定线程安全。锁的范围如果没有覆盖完整不变量,或者存在绕过锁的访问路径,仍然不安全。
  • 误以为读操作不需要加锁。读操作如果依赖一致状态或可见性,也需要同一同步协议或等价机制。
  • 误把 ReentrantLock 理解成锁住某个对象。它的核心是 AQS state、持有线程和等待队列等同步器状态。

面试官追问

synchronized 修饰普通方法和静态方法锁的一样吗?

不一样。普通同步方法锁的是当前实例对象的 monitor;静态同步方法锁的是当前类对应的 Class 对象 monitor。它们不是同一把锁,所以默认不会互斥。

两个线程访问同一个对象的不同 synchronized 方法会互斥吗?

如果这两个方法都是普通 synchronized 方法,并且访问的是同一个实例对象,那么会互斥,因为它们竞争的是同一个实例 monitor。如果是不同实例对象,则不会互斥。

为什么说只给写操作加锁可能仍然不安全?

因为读操作如果不使用同一把锁,可能读到写操作过程中的中间状态,也可能因为内存可见性问题读到旧值。只要读操作参与共享不变量判断,就应该纳入同一同步协议,或者使用其他等价的并发控制手段。

锁住 this 和锁住一个 private final Object 有什么区别?

锁住 this 会暴露锁对象,外部代码也可能 synchronized 这个实例,造成意外阻塞或死锁。使用私有 final 锁对象能把同步协议封装在类内部,减少外部干扰,通常更可控。

如果每次进入方法都 new 一个锁对象,会发生什么?

通常不会起到互斥效果。因为每个线程拿到的都是不同锁对象,竞争的同步状态不同,彼此不会阻塞。锁对象必须被相关访问路径共享,才能保护同一份共享状态。