真实面经题目 · 原创解析
线程锁锁的到底是什么?
线程锁并不是把某个线程本身锁住,也不是直接把一段代码或一个变量物理锁住。更准确地说,锁是一种同步协议:它通过某个可竞争的同步状态,约束多个线程进入临界区的顺序,从而保护共享资源及其不变量。Java 中 synchronized 竞争的是对象监视器或类对象监视器,ReentrantLock 竞争的是基于 AQS 维护的同步状态;操作系统层面的 mutex、semaphore 等竞争的是内核或用户态维护的同步状态。
真实面经题目 · 原创解析
线程锁并不是把某个线程本身锁住,也不是直接把一段代码或一个变量物理锁住。更准确地说,锁是一种同步协议:它通过某个可竞争的同步状态,约束多个线程进入临界区的顺序,从而保护共享资源及其不变量。Java 中 synchronized 竞争的是对象监视器或类对象监视器,ReentrantLock 竞争的是基于 AQS 维护的同步状态;操作系统层面的 mutex、semaphore 等竞争的是内核或用户态维护的同步状态。
可以这样回答:线程锁锁的不是线程本身,而是某个同步状态;它真正保护的是临界区里共享资源的一致性。比如 Java 的 synchronized 加在普通方法或代码块上时,竞争的是某个对象的 monitor;加在静态方法或类对象上时,竞争的是 Class 对象对应的 monitor。ReentrantLock 则不是对象监视器,而是通过 AQS 的 state 和等待队列来表达是否被占用、被谁持有、是否可重入。再往下看,操作系统里的互斥量、信号量也不是锁住资源实体,而是锁住一份同步元数据,让线程根据这份状态阻塞、唤醒或继续执行。所以说锁锁住资源只是简化说法,严谨表达应该是:锁通过同步状态限制并发访问,保护共享资源的不变量;只有所有相关读写路径都使用同一把锁,这个保护才成立。
线程锁容易被误解为把线程关起来。实际上线程是执行单元,锁影响的是线程能不能继续进入某段受保护的逻辑。当一个线程拿不到锁时,它可能自旋、阻塞、挂起或进入等待队列;这只是调度结果,不代表锁的对象是线程。锁的核心语义是:在同一时刻,只允许满足条件的线程进入临界区执行。
临界区通常会读写共享变量、集合、缓存、连接状态、余额、库存、任务队列等。真正需要保护的不是某一行代码,而是这些共享资源之间必须始终成立的关系,也就是不变量。例如数量不能为负、map 与 list 必须同步更新、状态从未开始到进行中再到完成不能倒退。锁的价值是让一组操作看起来像一个不可被打断的整体。
在 Java 里,synchronized 的锁粒度取决于它绑定的对象。普通同步方法默认使用当前实例对象的 monitor;同步代码块使用括号里指定对象的 monitor;静态同步方法使用对应 Class 对象的 monitor。这里的锁不是方法本身,也不是变量本身,而是某个对象关联的监视器。只要两个 synchronized 片段使用的是同一个监视器,它们就会互斥。
ReentrantLock 的实现思路不同于 synchronized。它通过 AQS 维护一个 state 字段表示锁的占用次数,通过 owner 记录当前持有线程,并通过等待队列管理获取失败的线程。可重入的含义是同一线程再次获取锁时 state 递增,释放时递减,直到归零才真正释放。这里被竞争的本质是同步器内部的状态。
在更底层,mutex、semaphore、读写锁、条件变量等同步原语也不会直接把某个资源实体锁住。它们维护的是计数、拥有者、等待队列、唤醒关系等同步状态。线程根据这些状态决定继续运行、进入内核阻塞、在用户态自旋,或被其他线程唤醒。不同实现可能有用户态快路径和内核态慢路径,但原则相同。
锁的正确性依赖约定。假设一个共享对象有两条访问路径,一条加 lockA,另一条加 lockB,那么这两条路径之间并不互斥;如果还有一条路径完全不加锁,锁也无法阻止它读到中间态或写坏数据。因此判断锁是否有效,不能只看有没有加锁,而要看所有读写同一组共享状态的地方是否统一使用同一把锁。
在 Java 内存模型中,锁不仅提供互斥,还提供内存可见性。释放锁之前的写入,对随后获取同一把锁的线程可见。也就是说,同一把锁建立了 happens-before 关系,使线程不会只看到过期值或乱序导致的不一致状态。没有这层语义,即使线程没有同时执行临界区,也可能因为缓存和重排序看到错误结果。
不一样。普通同步方法锁的是当前实例对象的 monitor;静态同步方法锁的是当前类对应的 Class 对象 monitor。它们不是同一把锁,所以默认不会互斥。
如果这两个方法都是普通 synchronized 方法,并且访问的是同一个实例对象,那么会互斥,因为它们竞争的是同一个实例 monitor。如果是不同实例对象,则不会互斥。
因为读操作如果不使用同一把锁,可能读到写操作过程中的中间状态,也可能因为内存可见性问题读到旧值。只要读操作参与共享不变量判断,就应该纳入同一同步协议,或者使用其他等价的并发控制手段。
锁住 this 会暴露锁对象,外部代码也可能 synchronized 这个实例,造成意外阻塞或死锁。使用私有 final 锁对象能把同步协议封装在类内部,减少外部干扰,通常更可控。
通常不会起到互斥效果。因为每个线程拿到的都是不同锁对象,竞争的同步状态不同,彼此不会阻塞。锁对象必须被相关访问路径共享,才能保护同一份共享状态。