真实面经题目 · 原创解析

java中加锁的方式有哪些,怎么个写法?

Java 加锁常见方式包括 synchronized、ReentrantLock、ReadWriteLock、StampedLock,以及更轻量的原子类、并发容器、线程封闭等替代方案。面试回答不能只背 API,重点要说清楚锁住的对象、写法、释放方式、适用场景、风险和性能取舍。

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

60 秒回答模板

可以按“内置锁、显式锁、读写锁、乐观读锁、少用锁的替代方案”来回答。synchronized 是 JVM 内置锁,可以修饰实例方法、静态方法,也可以包住代码块;ReentrantLock 是显式锁,必须在 finally 中 unlock,适合需要可中断、超时尝试、公平锁、多个 Condition 的场景;ReentrantReadWriteLock 适合读多写少,把读锁和写锁分开;StampedLock 支持乐观读,适合读多写少且读操作很短的场景,但它不可重入,使用复杂度更高。实际开发中不能为了线程安全就到处加锁,要优先缩小共享状态范围,能用不可变对象、局部变量、并发集合、原子类、CAS、队列串行化解决的,就不要引入粗粒度互斥锁。

考点 先定义锁的目标
主线 synchronized 方法
易错点 只说 synchronized 和 Lock 的名字,…

深入解析

01

先定义锁的目标

加锁的本质是保护共享可变状态,而不是保护代码本身。面试中要先说明:多个线程是否会同时访问同一份数据;是否存在写操作;是否要求复合操作的原子性;锁的粒度是对象级、类级、字段级还是业务资源级。只有明确保护对象,才能判断应该锁 this、Class、私有锁对象,还是使用显式锁。

02

synchronized 方法

synchronized 是 Java 内置监视器锁。修饰实例方法时,锁的是当前对象 this;修饰 static 方法时,锁的是当前类的 Class 对象。它的优点是语法简单、自动释放、异常时不会忘记解锁,并且具备可见性和互斥性。缺点是灵活性不如显式锁,不能直接做超时获取锁、公平锁选择、多个条件队列等复杂控制。

03

synchronized 代码块

synchronized 代码块可以精确控制临界区和锁对象。常见写法是 synchronized(lockObject) { ... }。工程上更推荐使用 private final Object lock = new Object() 作为锁对象,避免锁 this 或公共对象导致外部代码也能参与竞争。代码块的优势是粒度更小,只把真正访问共享状态的部分包起来,减少锁竞争。

04

ReentrantLock

ReentrantLock 是 java.util.concurrent.locks 包下的可重入显式锁。典型写法是 lock.lock(); try { 临界区 } finally { lock.unlock(); }。它适合需要高级控制的场景,例如 tryLock 避免死等、tryLock(timeout) 超时放弃、lockInterruptibly 响应中断、公平锁构造、配合 Condition 拆分等待队列。代价是必须手动释放锁,遗漏 finally 会造成严重故障。

05

ReadWriteLock

ReadWriteLock 的典型实现是 ReentrantReadWriteLock,核心思想是读读不互斥、读写互斥、写写互斥。它适合读多写少、读操作耗时明显、共享数据一致性要求明确的场景。写法上使用 readLock().lock() 保护读路径,使用 writeLock().lock() 保护写路径。需要注意读写锁不是万能优化,如果写很多、临界区很短、锁竞争不明显,读写锁反而可能更复杂更慢。

06

StampedLock

StampedLock 提供写锁、悲观读锁和乐观读。乐观读通常先 tryOptimisticRead 获取 stamp,读取字段后 validate(stamp) 校验期间是否发生写入;校验失败再退化为悲观读锁。它适合读多写少、读操作短、可以容忍重试的场景。它的限制也很重要:不可重入,使用 stamp 解锁,写法容易出错,不适合简单业务中随意替代 synchronized 或 ReentrantLock。

07

不要过度加锁

并发设计不应该把加锁作为唯一答案。能用局部变量、不可变对象、ThreadLocal、ConcurrentHashMap、BlockingQueue、AtomicInteger、LongAdder、CopyOnWriteArrayList、数据库唯一约束或消息队列串行化解决的问题,通常不需要手写互斥锁。锁的粒度过大容易降低吞吐,锁的顺序混乱容易死锁,锁住慢 IO 或远程调用会放大故障影响。

java

Java 常见加锁写法

class LockExamples {
    private final Object monitor = new Object();
    private final java.util.concurrent.locks.ReentrantLock lock =
            new java.util.concurrent.locks.ReentrantLock();
    private final java.util.concurrent.locks.ReadWriteLock rw =
            new java.util.concurrent.locks.ReentrantReadWriteLock();

    public synchronized void syncMethod() {
        // lock: this
    }

    public static synchronized void staticSyncMethod() {
        // lock: LockExamples.class
    }

    public void syncBlock() {
        synchronized (monitor) {
            // lock: private final monitor
        }
    }

    public void reentrantLockStyle() {
        lock.lock();
        try {
            // critical section
        } finally {
            lock.unlock();
        }
    }

    public void readWriteStyle() {
        rw.readLock().lock();
        try {
            // read shared state
        } finally {
            rw.readLock().unlock();
        }

        rw.writeLock().lock();
        try {
            // mutate shared state
        } finally {
            rw.writeLock().unlock();
        }
    }
}
  • synchronized 方法和代码块由 JVM 自动释放锁;显式锁必须把 unlock 放进 finally。
  • 示例使用 private final 锁对象,避免把 this 或公共对象暴露给外部代码参与竞争。

易错点

  • 只说 synchronized 和 Lock 的名字,不说明锁对象和写法。
  • 在 ReentrantLock 中忘记用 finally unlock。
  • 把 synchronized(this) 用在可被外部访问的对象上,扩大锁暴露范围。
  • 认为 volatile 可以替代锁完成 i++、检查再更新等复合操作。
  • 读写锁场景判断错误,在写多读少的代码里盲目使用 ReadWriteLock。
  • 持锁执行数据库、RPC、文件 IO 等慢操作,导致锁竞争和故障放大。
  • 忽略锁顺序,多个锁交叉获取造成死锁风险。

面试官追问

synchronized 和 ReentrantLock 怎么选?

普通互斥优先 synchronized,代码短、自动释放、可读性好。需要超时获取锁、可中断加锁、公平锁、多个 Condition 队列,或者需要更精细的锁控制时,选择 ReentrantLock。

synchronized 锁 this 有什么问题?

this 是外部可见对象,外部代码也可能 synchronized 同一个实例,导致意外阻塞或死锁。更稳妥的方式是使用 private final Object lock 作为内部锁对象。

什么是可重入锁?

同一个线程已经持有某把锁时,可以再次获取这把锁而不会被自己阻塞。synchronized 和 ReentrantLock 都是可重入锁。可重入能力常用于一个同步方法调用另一个同步方法的场景。

ReadWriteLock 一定比 synchronized 快吗?

不一定。它只在读多写少、读操作较重、竞争明显时更可能有收益。如果临界区很小、写操作多或锁竞争低,读写锁的管理成本可能超过收益。

加锁能保证可见性吗?

能。线程释放锁前对共享变量的修改,对之后获取同一把锁的线程可见。关键是必须围绕同一把锁访问同一份共享状态,否则可见性和互斥都不成立。

如何避免死锁?

常见做法是固定加锁顺序、减少锁嵌套、缩小临界区、不在持锁期间调用外部服务或未知回调,必要时使用 tryLock 超时失败后释放已持有的锁。