并发编程中,锁是保证线程安全的核心手段。Java提供了两种主要的锁机制:synchronized关键字和Lock接口。理解它们的原理、差异和适用场景,是写出高性能并发代码的关键。

一、synchronized:JVM内置锁

synchronized是Java最基础的同步机制,可以修饰方法或代码块。它由JVM直接实现,无需手动释放锁,异常时也会自动释放,因此不会出现死锁(除非嵌套不当)。其底层依赖对象头的Mark Word和Monitor(管程)。每个对象都关联一个Monitor,线程进入同步块时尝试获取Monitor的所有权。

为了减少锁带来的性能开销,JVM引入了锁升级机制:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁适用于同一线程反复进入同步块的场景,只需在Mark Word中记录线程ID,无需CAS操作。当有另一个线程竞争时,升级为轻量级锁,通过CAS自旋尝试获取锁。若自旋失败或竞争激烈,则膨胀为重量级锁,线程进入操作系统级别的阻塞队列,等待唤醒。这一过程不可逆,但有效平衡了不同竞争程度下的性能。

synchronized的优点是使用简单、由JVM自动优化,且Java 6之后性能大幅提升。缺点也很明显:不可中断、不支持超时、默认非公平,且无法知道锁是否被成功获取。

二、Lock接口:显式锁的灵活性

Lock是Java 5引入的显式锁接口,核心实现是ReentrantLock。与synchronized不同,Lock必须手动获取和释放,通常配合try-finally使用,否则一旦忘记unlock()就会导致死锁。

Lock接口提供了更丰富的能力:

lock():获取锁,若被占用则阻塞。

tryLock():尝试非阻塞获取,可设置超时。

lockInterruptibly():可中断地获取锁,支持响应中断。

unlock():释放锁。

ReentrantLock内部基于AQS(AbstractQueuedSynchronizer)实现,通过一个volatile的state变量表示同步状态,并维护一个CLH双向队列存放等待线程。它支持公平锁和非公平锁:公平锁按请求顺序获取,避免饥饿但吞吐量略低;非公平锁允许插队,性能更高,也是默认模式。此外,ReentrantLock可以创建多个Condition对象,实现更精细的等待/通知机制,比Object.wait/notify更灵活。

三、两者对比与选型

性能方面,在低竞争场景下,synchronized经过锁升级优化后与ReentrantLock相差无几,甚至更优;高竞争时,ReentrantLock的可中断和超时机制能避免线程长时间阻塞。功能方面,Lock明显更强大:支持公平锁、可中断、可超时、多条件变量。但synchronized胜在简洁、安全,不易出错。

因此,选型原则很简单:如果只是简单的互斥同步,优先用synchronized,代码更干净,JVM也会持续优化。如果需要超时、中断、公平锁或多条件等待,则选择Lock。此外,ReadWriteLock读写锁适合读多写少的场景,StampedLock则提供了乐观读,进一步提升并发性能。

四、总结

从synchronized到Lock,Java锁机制经历了从内置到显式、从单一到灵活的演进。理解锁升级和AQS原理,不仅能帮助我们在面试中脱颖而出,更能在实际开发中做出合理选择。锁不是越多越好,粒度也不是越细越好——根据竞争程度和业务需求权衡,才是并发编程的精髓。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐