在 Java 中,synchronized 和 Lock 都是用来实现线程同步、保证共享资源安全访问的机制。它们解决的是同一个问题------多个线程同时访问临界区时的互斥与可见性,但在使用方式、灵活性和底层实现上有明显区别。
下面从几个维度来对比和讲解。
一、synchronized
synchronized 是 Java 语言内置的关键字,属于隐式锁,由 JVM 底层实现。
1. 三种用法
java
// 1. 修饰实例方法:锁的是当前对象 this
public synchronized void method() { ... }
// 2. 修饰静态方法:锁的是当前类的 Class 对象
public static synchronized void staticMethod() { ... }
// 3. 修饰代码块:锁的是括号里指定的对象
public void block() {
synchronized (lockObj) {
// 临界区
}
}
2. 特点
- 自动加锁/释放锁:进入同步块自动加锁,退出(正常或异常)自动释放,不会忘记解锁。
- 可重入:同一线程可以重复获取同一把锁。
- 不可中断:等待锁的线程无法被中断,只能一直等。
- 非公平:默认是非公平锁(不保证等待时间长的线程优先获得锁)。
- 无法尝试获取:不能"试一下拿不到就走",只能阻塞等待。
- 底层实现:早期是重量级锁(依赖操作系统 mutex),JDK 1.6 后引入了偏向锁、轻量级锁、自旋优化等,性能大幅提升。
二、Lock 接口
Lock 是 java.util.concurrent.locks 包下的接口,属于显式锁,需要手动加锁和释放。
1. 核心方法
java
public interface Lock {
void lock(); // 获取锁,拿不到就阻塞
void lockInterruptibly() throws InterruptedException; // 可中断地获取锁
boolean tryLock(); // 尝试获取,立即返回成功/失败
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 限时尝试
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
2. 标准使用模板
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在 finally 中释放
}
3. 常用实现类
- ReentrantLock:可重入锁,最常用。
- ReentrantReadWriteLock:读写锁,读读不互斥,读写/写写互斥。
- StampedLock:JDK 8 引入,支持乐观读,性能更好。
三、核心区别对比
| 维度 | synchronized | Lock |
|---|---|---|
| 层级 | JVM 关键字,语言层面 | JDK 接口,API 层面 |
| 锁的获取/释放 | 自动 | 手动,必须 finally 释放 |
| 可中断 | 不支持 | lockInterruptibly() 支持 |
| 尝试获取 | 不支持 | tryLock() 支持 |
| 超时获取 | 不支持 | tryLock(time) 支持 |
| 公平性 | 只能非公平 | 可选公平/非公平 |
| 条件变量 | 配合 wait/notify,单条件 |
可创建多个 Condition |
| 读写分离 | 不支持 | ReadWriteLock 支持 |
| 可重入 | 支持 | ReentrantLock 支持 |
| 性能 | JDK 1.6 后优化良好 | 高竞争下更灵活可控 |
四、Condition:更精细的等待/通知
synchronized 配合 Object 的 wait/notify 只有一个等待队列。而 Lock 可以创建多个 Condition,实现分组唤醒。
java
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (队列满) notFull.await();
// 生产...
notEmpty.signal();
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (队列空) notEmpty.await();
// 消费...
notFull.signal();
} finally {
lock.unlock();
}
这样生产者只唤醒消费者,消费者只唤醒生产者,避免无效唤醒,这也是实现阻塞队列的经典方式。
五、底层原理简述
synchronized
- 依赖对象头里的 Mark Word 和 Monitor(管程)。
- 锁状态升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
- 重量级锁会涉及用户态到内核态的切换,成本较高。
ReentrantLock
- 基于 AQS(AbstractQueuedSynchronizer) 实现。
- 内部维护一个
volatile int state表示锁状态,一个 CLH 双向队列存放等待线程。 - 加锁就是 CAS 修改 state,失败则入队并
LockSupport.park()阻塞。 - 解锁就是 state 减到 0 并唤醒队列中的后继节点。
六、如何选择
优先用 synchronized,因为:
- 写法简单,不易出错(自动释放)。
- JVM 持续优化,大多数场景性能足够。
- 代码可读性好。
以下情况考虑 Lock:
- 需要可中断、可超时、可尝试获取锁。
- 需要公平锁。
- 需要读写分离(读多写少场景用
ReadWriteLock)。 - 需要多个条件变量精细控制线程唤醒。
- 需要乐观读(
StampedLock)。
七、常见误区
- 锁对象选错 :
synchronized修饰方法锁的是this,如果多个不同实例,锁不住共享的静态资源。 - 忘记 unlock :
Lock必须在finally中释放,否则死锁。 - 锁粒度太大:把无关操作也放进同步块,降低并发度。
- 用 String 常量或基本类型包装类当锁:可能因常量池/缓存导致意外的锁共享。
- 以为 synchronized 一定慢:现代 JVM 下并非如此,不要盲目替换。
一句话总结 :synchronized 是简单、安全、够用的内置锁;Lock 是灵活、可控、功能更丰富的显式锁。日常优先 synchronized,需要高级特性时再上 Lock。