深入解析JCSprout项目中的ReentrantLock实现原理

深入解析JCSprout项目中的ReentrantLock实现原理

JCSprout 👨‍🎓 Java Core Sprout : basic, concurrent, algorithm JCSprout 项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout

前言

在多线程编程中,锁机制是保证线程安全的重要手段。Java提供了两种主要的锁实现方式:synchronized关键字和ReentrantLock类。本文将重点分析JCSprout项目中关于ReentrantLock的实现原理,帮助开发者深入理解这一重要并发工具的内部工作机制。

ReentrantLock概述

ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个重要类,它实现了Lock接口,提供了比synchronized更灵活的锁操作。与synchronized相比,ReentrantLock具有以下特点:

  1. 显式加锁和解锁,需要手动调用lock()和unlock()方法
  2. 支持公平锁和非公平锁两种模式
  3. 提供可中断的获取锁机制
  4. 支持尝试获取锁和超时获取锁
  5. 提供更丰富的条件变量支持

重入锁特性

ReentrantLock是一个可重入锁,这意味着:

  • 同一个线程可以多次获得同一把锁
  • 锁内部维护一个计数器记录重入次数
  • 必须释放相同次数的锁才能真正释放

这种特性避免了线程自己阻塞自己的情况,使得递归调用和锁的重入成为可能。

锁的公平性

ReentrantLock提供了两种公平性策略:

1. 非公平锁(默认)

public ReentrantLock() {
    sync = new NonfairSync();
}

非公平锁的特点是:

  • 新来的线程可以直接尝试获取锁,不必排队
  • 吞吐量高,但可能导致某些线程长时间获取不到锁
  • 实现简单,性能好

2. 公平锁

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁的特点是:

  • 严格按照FIFO顺序获取锁
  • 避免线程饥饿现象
  • 性能相对较低,因为需要维护队列顺序

核心实现原理

ReentrantLock的实现依赖于AQS(AbstractQueuedSynchronizer)框架。AQS是Java并发包中实现锁和同步器的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

获取锁流程

公平锁获取流程
  1. 调用lock()方法
  2. 通过acquire(1)尝试获取锁
  3. tryAcquire()方法中:
    • 检查state是否为0(锁是否可用)
    • 检查队列中是否有等待线程(公平性保证)
    • CAS设置state
    • 设置独占线程
  4. 如果获取失败,将线程包装为Node加入队列
  5. 在队列中自旋或挂起等待
非公平锁获取流程
  1. 首先直接尝试CAS获取锁(插队行为)
  2. 如果失败,走类似公平锁的流程
  3. 关键区别:tryAcquire()中不检查队列情况

释放锁流程

  1. 调用unlock()方法
  2. tryRelease()减少state计数
  3. 当state减为0时,完全释放锁
  4. 唤醒队列中的下一个等待线程

关键源码分析

锁获取的核心方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法实现了获取锁的标准流程:

  1. 首先尝试快速获取锁(tryAcquire)
  2. 如果失败,将线程加入等待队列(addWaiter)
  3. 在队列中等待获取锁(acquireQueued)
  4. 如果等待过程中被中断,重新标记中断状态

队列管理

AQS使用CLH队列的变体来管理等待线程,主要方法包括:

  • addWaiter(): 将当前线程包装为Node加入队列尾部
  • enq(): 通过自旋+CAS确保节点正确入队
  • shouldParkAfterFailedAcquire(): 检查并更新前驱节点的waitStatus

线程挂起与唤醒

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

使用LockSupport.park()挂起线程,这是比Object.wait()更底层的线程阻塞机制。

性能比较

公平锁和非公平锁的主要性能差异源于:

  1. 公平锁需要严格维护FIFO顺序,导致更多的上下文切换
  2. 非公平锁允许插队,减少了线程切换的开销
  3. 在竞争激烈的情况下,非公平锁的吞吐量明显更高

因此,在大多数场景下,非公平锁是更好的选择,这也是ReentrantLock默认使用非公平锁的原因。

最佳实践

  1. 总是使用try-finally块确保锁被释放
  2. 考虑使用tryLock()避免死锁
  3. 在持有锁时不要调用可能阻塞的方法
  4. 合理选择公平性策略
  5. 对于复杂条件等待,考虑使用Condition

总结

通过分析JCSprout项目中的ReentrantLock实现,我们可以深入理解:

  • AQS框架如何支撑锁的实现
  • 公平锁与非公平锁的区别与实现
  • 可重入特性的实现机制
  • 线程排队与唤醒的细节

理解这些底层原理有助于我们更好地使用并发工具,编写高效、安全的并发程序。

JCSprout 👨‍🎓 Java Core Sprout : basic, concurrent, algorithm JCSprout 项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花淑云Nell

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值