1. 同步机制的本质差异
在Java并发编程中,synchronized和Lock代表了两种截然不同的同步实现哲学。synchronized作为Java语言内置的关键字,其设计初衷是提供一种简单、易用的线程同步机制。当我们在方法或代码块前加上synchronized修饰符时,JVM会在字节码层面自动插入monitorenter和monitorexit指令,这种实现方式使得开发者无需关心底层细节。
相比之下,Lock是一个显式的接口,其实现类如ReentrantLock提供了更丰富的API操作。这种设计将同步控制的权力完全交给了开发者,需要手动调用lock()和unlock()方法。我在实际项目中发现,这种显式控制虽然增加了代码复杂度,但在复杂场景下能提供更灵活的同步策略。
从底层实现来看,synchronized的锁信息存储在对象头中的Mark Word里,而Lock则完全通过Java代码实现。这种差异导致它们在内存占用、性能表现上各有优劣。特别是在JDK1.6之后,synchronized引入了偏向锁、轻量级锁等优化,使得它在低竞争场景下性能接近Lock。
关键提示:选择synchronized还是Lock,首先要考虑的是团队的技术水平和项目复杂度。简单的同步需求用synchronized更稳妥,而需要精细控制锁的场景则应该选择Lock。
2. 锁的获取与释放机制
锁的获取方式上,synchronized采用的是隐式获取机制。当线程进入synchronized修饰的方法或代码块时,锁会自动获取;当线程退出时,无论正常退出还是异常退出,锁都会自动释放。这种机制虽然安全,但也带来了灵活性不足的问题。
Lock则要求开发者显式调用lock()方法获取锁,并通过unlock()方法释放锁。这种设计带来了更大的控制权,但也引入了新的风险。我曾在项目中遇到过因忘记调用unlock()导致的死锁问题,后来通过try-finally块确保锁的释放:
java复制Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
在锁的释放时机上,Lock还提供了更细粒度的控制。比如tryLock()方法可以尝试获取锁,如果锁不可用就立即返回而不是阻塞。这在避免死锁的场景中特别有用:
java复制if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 获取锁超时处理
}
3. 功能特性对比
synchronized作为语言原生支持的特性,功能相对单一但稳定可靠。它支持可重入性(一个线程可以多次获取同一个锁),但不支持中断、超时等高级功能。
Lock接口则定义了一组丰富的功能:
- 可中断的锁获取(lockInterruptibly())
- 超时获取锁(tryLock(long, TimeUnit))
- 公平锁与非公平锁选择
- 多个条件变量(Condition)支持
条件变量(Condition)是Lock提供的一个强大特性,它允许线程在不同的条件下等待。相比synchronized配合wait()/notify()的方式,Condition提供了更精确的线程唤醒控制:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待线程
lock.lock();
try {
while (!conditionSatisfied) {
condition.await();
}
// 处理业务
} finally {
lock.unlock();
}
// 唤醒线程
lock.lock();
try {
conditionSatisfied = true;
condition.signalAll();
} finally {
lock.unlock();
}
在实际项目中,我曾用Condition实现了一个高效的生产者-消费者模型,相比传统的synchronized方案,吞吐量提升了约30%。
4. 性能与适用场景
在JDK1.5时期,Lock的性能明显优于synchronized。但随着Java版本的迭代,synchronized经过多次优化(如偏向锁、适应性自旋等),在低竞争场景下性能已经与Lock相当。
从内存占用角度看,synchronized作为JVM内置机制,不需要额外创建对象,内存开销更小。而Lock实现类如ReentrantLock需要实例化对象,会占用更多内存。
根据我的经验,以下场景更适合使用synchronized:
- 简单的同步需求
- 锁竞争不激烈的场景
- 需要减少内存占用的场景
- 团队对并发编程经验不足时
而以下场景则应该考虑使用Lock:
- 需要尝试非阻塞地获取锁(tryLock)
- 需要可中断的锁获取
- 需要超时获取锁
- 需要公平锁策略
- 需要多个条件变量
- 需要获取锁的持有信息
在分布式系统开发中,Lock的灵活性使其更容易扩展为分布式锁。比如Redisson的分布式锁实现就是基于Lock接口规范,这为系统从单机扩展到集群提供了便利。
5. 实战中的陷阱与最佳实践
在使用synchronized时,最常见的错误是锁对象选择不当。我见过很多开发者这样使用:
java复制// 错误示范 - 锁住了不同的对象
public void method() {
synchronized(new Object()) {
// 临界区代码
}
}
正确的做法应该是锁住一个长期存在的、所有线程共享的对象:
java复制private final Object lock = new Object();
public void method() {
synchronized(lock) {
// 临界区代码
}
}
对于Lock的使用,最大的风险在于忘记释放锁。我曾调试过一个内存泄漏问题,最终发现是因为异常路径没有执行unlock()。现在我的编码规范要求所有Lock的使用都必须放在try-finally块中。
另一个常见误区是过度使用锁。在很多场景下,使用并发容器(如ConcurrentHashMap)、原子变量(如AtomicInteger)或者volatile变量就能满足需求,完全不需要引入重量级的锁机制。
在性能优化方面,我总结了几个经验:
- 减小锁的粒度:尽量只锁必要的代码段
- 降低锁的持有时间:快速执行临界区代码
- 避免嵌套锁:容易导致死锁
- 考虑读写锁(ReadWriteLock)替代独占锁
6. 从JVM角度看锁实现
理解synchronized的JVM实现有助于我们更好地使用它。在HotSpot虚拟机中,synchronized的实现经历了多次演进:
- 偏向锁:针对无竞争场景优化,通过CAS操作在对象头中记录偏向线程ID
- 轻量级锁:当有轻微竞争时,通过自旋尝试获取锁
- 重量级锁:竞争激烈时,升级为操作系统层面的互斥量
这种锁升级策略使得synchronized能够适应不同竞争强度的场景。我们可以通过JVM参数来调整这些行为,比如:
- -XX:+UseBiasedLocking:启用/禁用偏向锁
- -XX:BiasedLockingStartupDelay:设置偏向锁延迟启用时间
- -XX:PreBlockSpin:设置自旋次数
相比之下,Lock的实现完全在Java层面,不涉及JVM的特殊处理。ReentrantLock内部使用AbstractQueuedSynchronizer(AQS)框架,通过CAS操作和队列管理来实现锁的获取与释放。
7. 死锁预防与诊断
无论是synchronized还是Lock,使用不当都可能导致死锁。我在生产环境遇到过最典型的死锁场景是:线程A持有锁1并请求锁2,同时线程B持有锁2并请求锁1。
对于synchronized导致的死锁,我们可以通过jstack工具获取线程转储来分析:
code复制"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f48740f7000 nid=0x1e03 waiting for monitor entry [0x00007f486b7f6000]
java.lang.Thread.State: BLOCKED (on object monitor at 0x000000076abbacd8)
at DeadlockExample$Resource.methodB(DeadlockExample.java:26)
- waiting to lock <0x000000076abbacd8> (a java.lang.Object)
at DeadlockExample$1.run(DeadlockExample.java:42)
"Thread-0" #11 prio=5 os_prio=0 tid=0x00007f48740f5000 nid=0x1e02 waiting for monitor entry [0x00007f486b8f7000]
java.lang.Thread.State: BLOCKED (on object monitor at 0x000000076abbacc8)
at DeadlockExample$Resource.methodA(DeadlockExample.java:16)
- waiting to lock <0x000000076abbacc8> (a java.lang.Object)
at DeadlockExample$1.run(DeadlockExample.java:37)
Lock提供的tryLock()方法可以有效预防死锁。我们可以实现一个安全获取多个锁的工具方法:
java复制public static boolean acquireLocks(Lock... locks) {
boolean acquiredAll = true;
for (Lock lock : locks) {
if (!lock.tryLock()) {
acquiredAll = false;
break;
}
}
if (!acquiredAll) {
// 释放已经获取的锁
for (Lock lock : locks) {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
return acquiredAll;
}
在实际项目中,我还发现使用锁顺序协议(所有线程按固定顺序获取锁)能有效避免死锁。这需要团队在代码审查时特别注意锁的获取顺序。
