Java 中常见的锁与最佳实践
作者:数据
本文介绍了Java中常见的锁,包括语法/实现层面的synchronized关键字、Lock接口及其实现类,讨论了锁的特性、常见误区与最佳实践,感兴趣的朋友跟随小编一起看看吧
Java 中的锁可以从多个维度来划分,常见的锁包括:
- 语法/实现层面:
synchronized关键字(内置锁)、Lock接口及其实现类(如ReentrantLock、ReentrantReadWriteLock、StampedLock)。 - 特性层面:可重入锁、公平锁/非公平锁、读写锁、乐观锁/悲观锁、自旋锁、分段锁等。
- 并发工具包中的辅助锁:
Semaphore(信号量,可看作共享锁)、CountDownLatch、CyclicBarrier等虽不是严格意义上的锁,但也用于控制线程协作。
下面我们从最常用的几个维度展开聊聊。
1. 从语法和 API 层面看
synchronized(内置锁)
- 使用方式:修饰实例方法、静态方法或代码块。
- 原理:基于对象头的 Mark Word 和 monitor 对象实现。JDK 1.6 以后引入了偏向锁、轻量级锁、重量级锁的升级过程,以优化性能。
- 特点:使用简单,自动释放锁(异常或正常退出时),可重入。但无法中断等待锁的线程,也无法设置超时。
- 最佳实践:适用于锁的竞争不激烈、代码简单的情况。比如单例模式的同步方法、简单的线程安全计数器。
Lock接口(显式锁)
最核心的实现是 ReentrantLock,它提供了比 synchronized 更灵活的锁操作。
ReentrantLock- 示例:
ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}- 可重入:同一个线程可以多次获得同一把锁。
- 公平/非公平:构造函数可指定是否公平。公平锁按线程等待顺序获取锁,非公平锁允许插队(默认非公平,吞吐量更高)。
- 可中断:
lockInterruptibly()允许在等待锁时响应中断。 - 超时获取:
tryLock(long time, TimeUnit unit)可以指定等待时间。 - 配合 Condition:可实现多个等待/通知条件,比 wait/notify 更精细。
ReentrantReadWriteLock- 维护一对锁:读锁(共享)和写锁(排他)。读多写少场景下可大幅提升并发度。
- 读锁可以被多个读线程同时持有,但写锁独占,且读写互斥。
- 可能出现“写饥饿”问题,但在非公平模式下,写锁优先级通常较高。
StampedLock(JDK 8 引入)- 进一步优化读多写少场景,支持三种模式:写锁、读锁、乐观读。
- 乐观读:不加锁,直接读取数据,之后通过 validate 检查是否有写操作发生。若无效再升级为读锁重读。
- 特点:不可重入,不支持条件变量,适合读线程远多于写线程的场景。
2. 从锁的特性层面看
- 悲观锁 vs 乐观锁
- 悲观锁:假设并发冲突概率高,每次操作都加锁(如 synchronized、ReentrantLock)。适合写多场景。
- 乐观锁:假设冲突少,先操作,提交时用 CAS(Compare And Swap)检测冲突,冲突则重试。如
java.util.concurrent.atomic包下的原子类。适合读多写少、冲突概率低的场景。
- 可重入锁
- 同一线程可以重复获取同一把锁,避免死锁。Java 中的 synchronized 和 ReentrantLock 都是可重入的。
- 公平锁 vs 非公平锁
- 公平锁按线程请求锁的顺序获取锁,避免饥饿,但吞吐量较低;非公平锁允许插队,吞吐量更高,但可能导致某些线程长时间得不到锁。
- 自旋锁
- 线程获取锁失败时不立即挂起,而是循环尝试(自旋),减少线程上下文切换的开销。适合锁持有时间短的场景。JDK 内部大量使用了自旋锁(如
ConcurrentLinkedQueue的入队操作)。Java 中也提供了AbstractQueuedSynchronizer(AQS)对自旋和阻塞的支持。
- 线程获取锁失败时不立即挂起,而是循环尝试(自旋),减少线程上下文切换的开销。适合锁持有时间短的场景。JDK 内部大量使用了自旋锁(如
- 分段锁
ConcurrentHashMap在 JDK 1.7 中的实现,将数据分段,每段一把锁,降低锁粒度,提升并发度。JDK 1.8 改用 CAS + synchronized 对每个桶(数组元素)加锁,相当于进一步细化。
3. 并发工具包中的 “锁” 变体
Semaphore:计数信号量,可以控制同时访问资源的线程数量(共享锁)。比如限流场景。CountDownLatch:让一个或多个线程等待,直到一组操作完成。CyclicBarrier:让一组线程互相等待,到达某个屏障点后再同时执行。
虽然这些不是传统意义上的锁,但它们也常被用来解决线程协作问题,面试时可以作为补充提及。
常见误区与最佳实践
- 误区一:认为 synchronized 性能总比 ReentrantLock 差。实际上 JDK 1.6 以后 synchronized 经过优化,性能与 ReentrantLock 差距不大,甚至在低竞争时更好。选择时应更关注功能需求。
- 误区二:滥用读写锁。如果读操作很短,或者写操作频繁,读写锁可能不如普通互斥锁,因为维护读锁和写锁本身也有开销。
- 误区三:忘记在 finally 中释放显式锁。这是新手最容易犯的错误,可能导致死锁。
- 最佳实践:
- 优先使用 synchronized,简洁安全;需要高级功能(如可中断、超时、公平性、多 Condition)时才选用 ReentrantLock。
- 读多写少且数据一致性要求不那么极致时,可考虑 StampedLock 的乐观读模式,或使用 CopyOnWrite 容器。
- 对于简单的原子操作,优先使用 Atomic 系列类(乐观锁思想),避免锁开销。
- 了解 JVM 的锁优化(如偏向锁、轻量级锁),有助于调优高并发应用。
到此这篇关于Java 中常见的锁有哪些?的文章就介绍到这了,更多相关java常见锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
