synchronized 与 ReentrantLock 区别?公平锁、非公平锁、可重入锁、自旋锁的原理与应用?

synchronized 与 ReentrantLock 区别?公平锁、非公平锁、可重入锁、自旋锁的原理与应用?

作为一名拥有八年 Java 后端开发经验的工程师,在多线程并发编程的战场上,锁机制始终是保障数据一致性和程序正确性的重要武器。其中,synchronized和ReentrantLock是我们最常使用的两种锁,而公平锁、非公平锁、可重入锁、自旋锁等概念,更是深入理解锁机制的关键。今天,我就从原理出发,结合实际业务场景,为大家详细剖析它们的区别与应用。

一、synchronized 与 ReentrantLock 的区别

1. 实现原理

  • synchronized :这是 Java 的内置关键字,基于 JVM 实现。在 JVM 层面,它通过monitorentermonitorexit 指令来实现锁的获取和释放。当线程执行到monitorenter 指令时,如果对象的监视器(monitor)计数器为 0,说明该对象没有被锁定,线程就会获取该监视器并将计数器加 1;如果计数器不为 0,说明对象已被其他线程锁定,当前线程会进入阻塞状态。当线程执行到monitorexit指令,或者发生异常时,会释放监视器并将计数器减 1。
  • ReentrantLock:它是 Java SDK 层面的锁,基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器)实现。AQS 是一个用于构建锁和同步器的框架,它通过一个 FIFO 的双向队列来管理等待获取锁的线程。ReentrantLock内部维护了一个状态变量(state),用于记录锁的持有次数。当线程获取锁时,会尝试将 state 加 1,释放锁时将 state 减 1,当 state 为 0 时表示锁已完全释放。

2. 锁的特性

  • 可重入性:两者都具备可重入性。也就是说,同一个线程可以多次获取同一把锁而不会产生死锁。例如,在一个类中,一个 synchronized 方法调用了另一个 synchronized 方法,由于是同一个线程,不会出现死锁情况;ReentrantLock同样支持这种可重入特性,它通过记录锁的持有线程和持有次数来实现。
  • 公平性:synchronized是非公平锁,即线程获取锁的顺序是不确定的,新到来的线程有可能在等待队列中的线程之前获取到锁。而ReentrantLock默认也是非公平锁,但它可以通过构造函数ReentrantLock(true)来创建公平锁,公平锁会严格按照线程在等待队列中的顺序来分配锁,先进入等待队列的线程先获取锁。
  • 锁的获取与释放:synchronized在代码块执行完毕或者抛出异常时,会自动释放锁,无需手动操作;而ReentrantLock需要显式地调用lock()方法获取锁,调用unlock()方法释放锁,并且为了保证锁一定能被释放,通常会将unlock()方法放在finally块中。
csharp 复制代码
// synchronized示例
public class SynchronizedExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}
// ReentrantLock示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

3. 功能特性

ReentrantLock相比synchronized提供了更多的功能。例如,它支持尝试锁定 ,通过tryLock()方法,线程可以尝试获取锁,如果获取成功则返回true,否则返回false,这样可以避免线程长时间等待;还支持定时锁定 ,通过tryLock(long time, TimeUnit unit)方法,线程在指定时间内尝试获取锁,超过时间则返回false;此外,ReentrantLock还可以创建多个条件变量(Condition),通过newCondition()方法创建,用于实现更灵活的线程间通信,而synchronized只能使用wait()、notify()和notifyAll()进行线程通信。

二、公平锁、非公平锁、可重入锁、自旋锁的原理与应用

1. 公平锁与非公平锁

  • 原理:公平锁保证线程获取锁的顺序和它们在等待队列中的顺序一致,新线程必须加入到等待队列的末尾等待,只有当前持有锁的线程释放锁后,等待队列中最前面的线程才能获取锁。而非公平锁则允许新线程在尝试获取锁时,直接竞争锁,如果此时锁可用,新线程就可以立即获取锁,而无需进入等待队列,这样可能导致等待队列中的线程长时间无法获取锁,出现 "饥饿" 现象。
  • 应用场景:公平锁适用于对线程公平性要求较高的场景,比如某些资源分配场景,需要保证每个线程都能按照申请的顺序获取资源;但由于公平锁在每次锁释放后都需要唤醒等待队列中最前面的线程,会带来一定的性能开销。非公平锁在大多数情况下性能更好,因为它减少了线程切换的开销,适用于对公平性要求不高,更注重性能的场景,例如高并发的缓存访问场景,大多数情况下非公平锁能提供更好的吞吐量。

2. 可重入锁

  • 原理:可重入锁允许同一个线程多次获取同一把锁。当线程获取锁时,会记录锁的持有线程和持有次数,每次重入时,持有次数加 1,释放锁时持有次数减 1,只有当持有次数为 0 时,锁才会被真正释放。这样可以避免同一个线程在调用嵌套的同步方法或代码块时产生死锁。
  • 应用场景:在实际开发中,可重入锁的应用非常广泛。比如在一个业务逻辑复杂的类中,多个方法可能都需要对同一个资源进行同步访问,并且这些方法之间可能存在调用关系,使用可重入锁可以保证在这种嵌套调用的情况下不会出现死锁问题,同时保证数据的一致性。

3. 自旋锁

  • 原理:自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁已经被其他线程持有,该线程不会立即进入阻塞状态,而是在原地循环等待,不断检查锁是否已经释放,这种循环等待的操作就称为 "自旋"。如果在自旋的过程中锁被释放,那么该线程就可以立即获取锁,从而避免了线程上下文切换的开销。但如果自旋时间过长,会浪费 CPU 资源。
  • 应用场景:自旋锁适用于锁被占用的时间很短的场景,因为在这种情况下,线程通过自旋等待锁的释放,比进入阻塞状态再被唤醒的效率更高。例如,在一些高频的、短时间的资源竞争场景中,如 JVM 的同步块优化、CAS(Compare And Swap)操作等,自旋锁可以有效提高性能。但如果锁被占用的时间较长,自旋锁会导致 CPU 利用率过高,此时使用传统的阻塞锁可能更合适。

三、总结与选择建议

在八年 Java 后端开发实战中,我深刻体会到,锁机制的选择从来不是一道非此即彼的选择题,而是需要权衡业务场景、性能指标与开发成本的综合考量。以下从实际开发场景出发,为不同需求提供更具象化的锁选择指南。

1. synchronized 与 ReentrantLock 的抉择

  • 「即开即用」的快速场景:当项目中存在大量简单的同步需求,例如单例模式的双重检查锁定、方法级别的资源同步,且对公平性、锁超时等特性无特殊要求时,优先选择synchronized。它无需手动管理锁的生命周期,借助 JVM 内置的锁膨胀优化(偏向锁→轻量级锁→重量级锁),能在高并发下平稳过渡,适合追求开发效率的中小型项目。
  • 「精细控制」的复杂场景:若业务涉及分布式限流(需结合tryLock实现令牌获取)、线程池任务优先级调度(通过Condition精准唤醒特定线程),或对锁公平性有严格要求(如排队叫号系统),ReentrantLock凭借其灵活的 API 与可配置特性,成为更好的选择。特别是在微服务架构中,对锁的可观测性和可定制性需求更高时,ReentrantLock能提供更丰富的扩展空间。

2. 锁类型的场景化应用

  • 公平锁 vs 非公平锁:在秒杀系统的库存扣减环节,为避免 "插队" 导致部分用户始终无法获取资源,可使用公平锁保障用户公平性;而在高并发的商品浏览计数场景中,非公平锁能以最小的线程切换开销快速响应请求,显著提升系统吞吐量。
  • 可重入锁的必要性:在分层架构的业务逻辑中,如 Service 层方法调用 DAO 层事务方法时,可重入锁能避免因递归加锁导致的死锁问题。尤其在复杂的业务流程编排中,可重入特性是保证调用链正常执行的关键。
  • 自旋锁的性能博弈:在缓存更新的短时间资源竞争场景(如 Redis 的 SETNX 操作),自旋锁通过减少线程上下文切换,能大幅提升操作效率;但在数据库连接池获取连接这类耗时操作中,使用自旋锁会导致 CPU 空转,此时阻塞锁反而能优化系统资源利用率。

3. 性能与维护的平衡之道

实际项目中,锁机制的选择需兼顾性能优化与代码可维护性。例如,过度追求公平锁的严格顺序可能导致系统吞吐量下降,可通过定期重置等待队列优先级来折中;而频繁使用自旋锁时,应结合自适应自旋策略(根据历史锁占用时间动态调整自旋次数),避免 CPU 资源浪费。同时,建议通过 AOP 统一管理锁的获取与释放,降低代码侵入性,提升系统可维护性。

锁机制的选择是多线程编程的核心命题,唯有深入理解每种锁的原理与适用边界,结合业务场景进行动态调整,才能打造出既高效又可靠的并发系统。如果你在实际应用中遇到锁相关的性能瓶颈或疑难问题,欢迎随时交流探讨,共同探索更优的解决方案。

相关推荐
方圆想当图灵6 分钟前
深入理解软件设计:领域驱动设计 DDD
后端·架构
excel17 分钟前
MySQL 9 在 Windows 上使用 mysqld --initialize-insecure 无响应的排查与解决方案
后端
你怎么知道我是队长21 分钟前
GO语言---defer关键字
开发语言·后端·golang
方圆想当图灵34 分钟前
深入理解软件设计:什么是好的架构?
后端·架构·代码规范
fajianchen1 小时前
Spring中观察者模式的应用
java·开发语言
库库林_沙琪马1 小时前
深入理解 @JsonGetter:精准掌控前端返回数据格式!
java·前端
手握风云-1 小时前
JavaEE初阶第一期:计算机是如何 “思考” 的(上)
java·java-ee
普通的冒险者1 小时前
微博项目(总体搭建)
java·开发语言
love530love2 小时前
是否需要预先安装 CUDA Toolkit?——按使用场景分级推荐及进阶说明
linux·运维·前端·人工智能·windows·后端·nlp
BAGAE2 小时前
Flutter 与原生技术(Objective-C/Swift,java)的关系
java·开发语言·macos·objective-c·cocoa·智慧城市·hbase