Java重入锁(ReentrantLock)全面解析:从入门到源码深度剖析

Java重入锁(ReentrantLock)全面解析:从入门到源码深度剖析
在这里插入图片描述

文章目录

在这里插入图片描述

引言

在多线程编程的世界里,锁是最核心的同步工具之一。Java从语言层面提供了synchronized关键字来实现线程同步,简单而有效。然而,随着并发需求的复杂化,synchronized的局限性逐渐显现——它无法响应中断、无法设置超时、默认非公平且灵活性不足。为了解决这些问题,Java在java.util.concurrent.locks包中提供了ReentrantLock(重入锁),一个功能更强大、使用更灵活的锁工具。

本文将带你全方位地认识ReentrantLock,从基本概念到高级特性,从使用方式到源码剖析,从底层原理到实际应用。无论你是初学者还是希望深入理解并发编程的开发者,相信都能从中获得启发。全文约8500字,建议结合实践阅读。


第一部分:重入锁基础概念

1.1 什么是重入锁?

重入锁(Reentrant Lock),顾名思义,就是支持重入特性的锁。重入是指:同一个线程在持有锁的情况下,可以多次获取同一把锁而不会被阻塞

举个例子:如果一个线程已经获得了某个对象的锁,当它再次请求该对象的锁时,会直接成功,而不是死锁等待。这种机制在递归方法调用或嵌套同步块中至关重要。

publicclassReentrantExample{privatefinalObject lock =newObject();publicvoidmethodA(){synchronized(lock){// 已经持有锁methodB();// 再次请求同一把锁}}publicvoidmethodB(){synchronized(lock){// 这里不会死锁,因为synchronized是可重入的System.out.println("methodB执行");}}}

ReentrantLock同样支持这种重入特性,但它提供了比synchronized更丰富的功能。

1.2 为什么需要重入锁?

synchronized作为Java内置的关键字,使用简单,由JVM自动加锁和解锁,且经过多年的优化(偏向锁、轻量级锁、重量级锁升级),性能已经不逊色于ReentrantLock。既然如此,为什么还需要ReentrantLock

这是因为ReentrantLock弥补了synchronized的几个功能性缺陷

特性synchronizedReentrantLock
使用方式关键字,自动释放API调用,需手动释放
锁获取响应中断不支持支持(lockInterruptibly()
尝试获取锁不支持支持(tryLock()
超时获取锁不支持支持(tryLock(long, TimeUnit)
公平锁非公平可设置公平/非公平
条件变量每个对象一个等待集一个锁可绑定多个Condition
获取锁状态无法得知可查询持有线程、等待队列等

简单来说,当需要更精细的控制同步行为时,ReentrantLock是更好的选择

1.3 ReentrantLock的基本用法

在深入原理之前,我们先来看看ReentrantLock的标准使用模式:

importjava.util.concurrent.locks.ReentrantLock;publicclassCounter{privatefinalReentrantLock lock =newReentrantLock();privateint count =0;publicvoidincrement(){ lock.lock();// 获取锁try{ count++;}finally{ lock.unlock();// 必须在finally中释放锁!}}publicintgetCount(){ lock.lock();try{return count;}finally{ lock.unlock();}}}

核心要点

  • lock()unlock()必须成对出现
  • 解锁操作必须放在finally块中,确保无论是否发生异常都能释放锁
  • 不能在try块中调用lock(),因为lock()本身可能抛出异常

第二部分:ReentrantLock的核心特性

2.1 可重入性

可重入性是ReentrantLock命名中的核心特性。它通过计数机制实现:

publicclassReentrantDemo{privatefinalReentrantLock lock =newReentrantLock();publicvoidouter(){ lock.lock();try{System.out.println("外层方法获取锁");inner();}finally{ lock.unlock();}}publicvoidinner(){ lock.lock();// 同一线程再次获取锁try{System.out.println("内层方法再次获取锁");}finally{ lock.unlock();}}}

内部原理:每个锁关联一个持有线程和一个计数器。当线程第一次获取锁时,计数器置为1;同一个线程再次获取锁时,计数器递增;每释放一次,计数器递减;当计数器归零时,锁完全释放,其他线程才能获取。

2.2 公平锁与非公平锁

2.2.1 概念解析
  • 公平锁(FairSync):线程按照请求锁的先后顺序(FIFO)获取锁,不会产生饥饿现象。
  • 非公平锁(NonfairSync):线程在获取锁时,允许"插队",即直接尝试抢占锁,如果抢占成功就直接获得锁,抢占失败才进入队列等待。

ReentrantLock默认使用非公平锁,但可以通过构造器参数设置为公平锁:

ReentrantLock fairLock =newReentrantLock(true);// 公平锁ReentrantLock unfairLock =newReentrantLock(false);// 非公平锁ReentrantLock defaultLock =newReentrantLock();// 默认非公平锁
2.2.2 为什么默认非公平锁?

非公平锁虽然可能导致线程饥饿,但性能更高。原因在于:

  • 公平锁需要维护严格的排队机制,线程唤醒有开销
  • 非公平锁减少了线程的挂起和唤醒次数
  • 在高并发场景下,非公平锁的吞吐量通常优于公平锁
2.2.3 源码层面的差异

我们来看看非公平锁的lock()方法:

// NonfairSync的lock方法finalvoidlock(){// 直接尝试抢占锁(插队)if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}

而公平锁的lock()方法:

// FairSync的lock方法finalvoidlock(){acquire(1);// 直接进入队列,没有抢占机会}

公平锁的tryAcquire方法中多了一个关键判断:

protectedfinalbooleantryAcquire(int acquires){finalThread current =Thread.currentThread();int c =getState();if(c ==0){// 公平锁的额外判断:队列中是否有前驱节点if(!hasQueuedPredecessors()&&compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);returntrue;}}// ... 重入逻辑returnfalse;}

hasQueuedPredecessors()检查队列中是否有等待时间更长的线程,确保严格FIFO。

2.3 可中断锁

synchronized在等待锁的过程中无法响应中断,而ReentrantLock提供了可中断的获取锁方式:

publicclassInterruptibleDemo{privatefinalReentrantLock lock =newReentrantLock();publicvoidperformTask()throwsInterruptedException{// 可响应中断的锁获取 lock.lockInterruptibly();try{// 执行需要同步的操作System.out.println(Thread.currentThread().getName()+" 获得锁");Thread.sleep(5000);}finally{ lock.unlock();}}publicstaticvoidmain(String[] args)throwsException{InterruptibleDemo demo =newInterruptibleDemo();Thread t1 =newThread(()->{try{ demo.performTask();}catch(InterruptedException e){System.out.println("线程1被中断");}});Thread t2 =newThread(()->{try{ demo.performTask();}catch(InterruptedException e){System.out.println("线程2被中断");}}); t1.start();Thread.sleep(100);// 确保t1先获得锁 t2.start();// 中断正在等待锁的t2 t2.interrupt();}}

当t2在等待锁时被中断,会立即抛出InterruptedException,从而有机会响应中断,而不是无限阻塞。

2.4 限时等待锁

在实际开发中,无限等待锁可能导致系统死锁或响应延迟。ReentrantLock提供了带超时的锁获取方法:

importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.ReentrantLock;publicclassTimeoutDemo{privatefinalReentrantLock lock =newReentrantLock();publicbooleantryExecute(){try{// 尝试在3秒内获取锁if(lock.tryLock(3,TimeUnit.SECONDS)){try{System.out.println(Thread.currentThread().getName()+" 获得锁");Thread.sleep(2000);// 模拟业务操作returntrue;}finally{ lock.unlock();}}else{System.out.println(Thread.currentThread().getName()+" 获取锁超时");returnfalse;}}catch(InterruptedException e){System.out.println("线程被中断");returnfalse;}}}

tryLock()还有无参版本:如果锁可用则立即获取,否则立即返回false,不会阻塞。

2.5 条件变量(Condition)

ConditionObjectwait()notify()notifyAll()方法分解为不同的条件对象,使得一个锁可以支持多个等待集,实现更精细的线程协作。

importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassBoundedBuffer{privatefinalReentrantLock lock =newReentrantLock();privatefinalCondition notFull = lock.newCondition();privatefinalCondition notEmpty = lock.newCondition();privatefinalObject[] items =newObject[10];privateint putIndex, takeIndex, count;publicvoidput(Object x)throwsInterruptedException{ lock.lock();try{// 当队列满时,等待notFull条件while(count == items.length){ notFull.await();// 释放锁,进入等待} items[putIndex]= x;if(++putIndex == items.length) putIndex =0; count++;// 通知等待notEmpty条件的线程 notEmpty.signal();}finally{ lock.unlock();}}publicObjecttake()throwsInterruptedException{ lock.lock();try{// 当队列空时,等待notEmpty条件while(count ==0){ notEmpty.await();}Object x = items[takeIndex];if(++takeIndex == items.length) takeIndex =0; count--;// 通知等待notFull条件的线程 notFull.signal();return x;}finally{ lock.unlock();}}}

优势:与synchronized相比,Condition可以创建多个等待集,更灵活地控制线程协作。


第三部分:ReentrantLock与synchronized的全面对比

3.1 异同点总结

比较维度synchronizedReentrantLock
实现方式JVM内置关键字Java API实现,基于AQS
锁释放自动释放(退出同步块)手动释放(需finally中unlock)
可重入性支持支持
公平性非公平可公平可非公平
响应中断不支持支持(lockInterruptibly)
超时获取不支持支持(tryLock带超时)
尝试获取不支持支持(tryLock无参)
条件变量每个对象一个等待集一个锁可多个Condition
锁状态查询无法查询可查询持有线程、等待队列长度等
性能JDK6后优化良好高竞争场景表现更优

3.2 如何选择?

根据实际场景选择:

  • 优先使用synchronized:当同步逻辑简单、不需要高级特性时。它简洁、不易出错,且JVM持续优化。
  • 需要公平锁:必须保证线程获取锁的顺序时。
  • 需要可中断锁:希望避免线程无限期阻塞时。
  • 需要超时获取锁:防止死锁或保证响应时间时。
  • 需要多个条件变量:生产者-消费者模式等复杂协作时。
  • 高竞争场景ReentrantLock在高并发下表现更好。

第四部分:ReentrantLock源码深度剖析

4.1 AQS基础:重入锁的基石

要理解ReentrantLock,必须先理解AQS(AbstractQueuedSynchronizer)。AQS是Java并发包的基石,ReentrantLockSemaphoreCountDownLatch等工具都基于它实现。

4.1.1 AQS的核心思想

AQS维护了两个核心元素:

  1. volatile int state:同步状态,对于ReentrantLock,state表示锁的持有次数(0表示未持有,≥1表示持有次数)。
  2. FIFO等待队列(CLH队列变体):用于存放获取锁失败的线程。

核心操作:通过CAS(Compare And Swap)原子性地修改state值,成功则获得锁,失败则进入等待队列。

4.1.2 AQS的关键方法
方法描述
tryAcquire(int arg)尝试获取锁,由子类实现
tryRelease(int arg)尝试释放锁,由子类实现
acquire(int arg)获取锁的模板方法
release(int arg)释放锁的模板方法

ReentrantLock内部类Sync继承AQS,并实现了tryAcquiretryRelease

4.2 非公平锁源码解析

4.2.1 加锁过程
// ReentrantLock.NonfairSyncfinalvoidlock(){// 第一步:直接尝试抢占锁(插队)if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);// 抢占失败,进入AQS流程}
  • compareAndSetState(0, 1):通过CAS尝试将state从0改为1。如果成功,表示当前线程直接抢到了锁,设置独占线程为当前线程。
  • 如果CAS失败(锁已被其他线程持有),调用acquire(1)进入AQS的获取流程。
// AbstractQueuedSynchronizer.acquirepublicfinalvoidacquire(int arg){if(!tryAcquire(arg)&&// 再次尝试获取(非公平版)acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 加入等待队列selfInterrupt();}

这里的tryAcquire调用的是NonfairSync实现的nonfairTryAcquire

finalbooleannonfairTryAcquire(int acquires){finalThread current =Thread.currentThread();int c =getState();if(c ==0){// state为0,说明锁空闲,再次尝试CAS抢占if(compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(current ==getExclusiveOwnerThread()){// 重入:同一线程再次获取锁int nextc = c + acquires;if(nextc <0)// overflowthrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;}

非公平的体现:即使线程已经进入等待队列,在tryAcquire阶段仍然会尝试CAS抢占,而不是严格排队。

4.2.2 入队等待

如果tryAcquire失败,则执行addWaiter将当前线程封装成Node加入等待队列尾部:

privateNodeaddWaiter(Node mode){Node node =newNode(Thread.currentThread(), mode);Node pred = tail;if(pred !=null){ node.prev = pred;if(compareAndSetTail(pred, node)){ pred.next = node;return node;}}enq(node);// 入队失败或有并发时,通过自旋CAS入队return node;}

然后执行acquireQueued,在队列中自旋等待:

finalbooleanacquireQueued(finalNode node,int arg){boolean failed =true;try{boolean interrupted =false;for(;;){finalNode p = node.predecessor();// 如果前驱是头节点,再次尝试获取锁if(p == head &&tryAcquire(arg)){setHead(node); p.next =null;// help GC failed =false;return interrupted;}// 检查是否需要挂起线程if(shouldParkAfterFailedAcquire(p, node)&&parkAndCheckInterrupt()) interrupted =true;}}finally{if(failed)cancelAcquire(node);}}

当线程获取锁失败时,会被park(挂起),等待前驱线程释放锁时unpark唤醒。

4.2.3 释放锁
// ReentrantLock.SyncprotectedfinalbooleantryRelease(int releases){int c =getState()- releases;if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();boolean free =false;if(c ==0){ free =true;setExclusiveOwnerThread(null);}setState(c);return free;}

释放锁时递减state,直到state归零才真正释放锁。然后AQS会唤醒队列中的下一个节点。

4.3 公平锁源码解析

公平锁的lock()方法直接调用acquire(1),没有抢占尝试:

// FairSync.lockfinalvoidlock(){acquire(1);}

公平锁的tryAcquire与非公平锁的核心区别在于多了hasQueuedPredecessors()判断:

protectedfinalbooleantryAcquire(int acquires){finalThread current =Thread.currentThread();int c =getState();if(c ==0){// 公平锁:检查队列中是否有前驱节点if(!hasQueuedPredecessors()&&compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);returntrue;}}// ... 重入逻辑returnfalse;}

hasQueuedPredecessors()判断当前线程之前是否有等待的线程,确保FIFO顺序。

4.4 限时获取锁的实现

tryLock(long timeout, TimeUnit unit)的底层通过doAcquireNanos实现:

privatebooleandoAcquireNanos(int arg,long nanosTimeout)throwsInterruptedException{long lastTime =System.nanoTime();finalNode node =addWaiter(Node.EXCLUSIVE);// ...for(;;){// 尝试获取锁// 计算剩余时间,超时则返回false nanosTimeout -=System.nanoTime()- lastTime;if(nanosTimeout <=0){cancelAcquire(node);returnfalse;}// 如果超时时间短,自旋;否则挂起if(shouldParkAfterFailedAcquire(p, node)&& nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// ...}}

通过LockSupport.parkNanos实现限时阻塞,超时后自动唤醒并返回失败。


第五部分:CAS与AQS——重入锁的底层基石

5.1 CAS操作

CAS(Compare And Swap)是并发编程中实现无锁算法的核心技术。它是一条CPU原子指令,包含三个操作数:

  • 内存地址V
  • 期望值A
  • 新值B

仅当V的值等于A时,才将V更新为B,整个过程原子完成。

在Java中,Unsafe类提供了CAS操作,ReentrantLock通过CAS修改AQS的state字段。

5.2 CAS的ABA问题

ABA问题:线程1读取变量值为A,此时线程2将A改为B再改回A,线程1CAS时发现仍是A,于是更新成功。但实际上变量已经被修改过。

解决方案:使用版本号或时间戳。Java提供了AtomicStampedReference来解决ABA问题。

5.3 AQS的设计精髓

AQS的核心设计理念包括:

  1. 模板方法模式:定义获取/释放锁的骨架,具体实现由子类完成。
  2. CLH队列变体:高效的双向队列管理等待线程。
  3. 状态依赖:通过state表示同步状态。
  4. 自旋与阻塞结合:短时间内自旋,长时间阻塞,平衡性能。
  5. LockSupport:提供线程挂起和唤醒的底层支持。

第六部分:实战应用与最佳实践

6.1 标准使用模板

publicclassSafeCounter{privatefinalReentrantLock lock =newReentrantLock();privateint count =0;publicvoidincrement(){ lock.lock();try{ count++;}finally{ lock.unlock();}}// 带超时的获取publicbooleantryIncrement(long timeout,TimeUnit unit){try{if(lock.tryLock(timeout, unit)){try{ count++;returntrue;}finally{ lock.unlock();}}returnfalse;}catch(InterruptedException e){Thread.currentThread().interrupt();returnfalse;}}}

6.2 监控与调试

ReentrantLock提供了监控锁状态的方法:

ReentrantLock lock =newReentrantLock();// 查询锁状态System.out.println("锁持有线程: "+ lock.getOwner());System.out.println("等待线程数: "+ lock.getQueueLength());System.out.println("是否被当前线程持有: "+ lock.isHeldByCurrentThread());System.out.println("是否公平锁: "+ lock.isFair());

这些方法对调试死锁、监控系统状态非常有帮助。

6.3 常见陷阱与注意事项

  1. 忘记释放锁:必须在finally中unlock。
  2. 在try块内lock:lock()可能抛出异常,应该先lock再try。
  3. 锁的可见性问题:ReentrantLock保证内存可见性,无需额外volatile。
  4. 重入计数溢出:重入次数受int范围限制,理论上可达21亿次。
  5. 与synchronized混用:不同锁机制之间不互斥,需注意设计。

6.4 性能考量

  • 低竞争场景:synchronized性能略优或持平
  • 高竞争场景:ReentrantLock性能更好
  • 公平锁性能低于非公平锁
  • 避免在锁内执行耗时操作

结语

ReentrantLock作为Java并发包中的核心工具,以其强大的功能和灵活的机制,成为高并发编程中不可或缺的利器。通过本文的学习,我们深入理解了:

  • 可重入性的实现原理
  • 公平锁与非公平锁的源码差异
  • 可中断、限时等待等高级特性
  • AQS作为底层的核心架构
  • 最佳实践与性能考量

掌握ReentrantLock不仅仅是学会使用一个类,更是理解Java并发编程思想的重要一步。在实际开发中,根据场景选择合适的同步工具,平衡功能与性能,才能写出高质量的多线程程序。

Read more

【 C++ 入门】Cyber骇客的 流式文本序列处理器 —— 【 string 类】万字大文带你从0学好C++的string类!

【 C++ 入门】Cyber骇客的 流式文本序列处理器 —— 【 string 类】万字大文带你从0学好C++的string类!

⚡ CYBER_PROFILE ⚡ /// SYSTEM READY /// [WARNING]: DETECTING HIGH ENERGY 🌊 🌉 🌊 心手合一 · 水到渠成 >>> ACCESS TERMINAL <<<[ 🦾 作者主页 ][ 🔥 C语言核心 ][ 💾 编程百度 ][ 📡 代码仓库 ] --------------------------------------- Running Process: 100% | Latency: 0ms 索引与导读 * 一、为什么学习 string类 ? * 二、C++ 标准库中的 string 类 * 2.1)auto和范围for * 2.2)string类的常用接口 * 🚩1)string类的常用构造 * 🚩2)string类对象的容量操作 * ❗注意事项 * 1)size(

By Ne0inhk
《C++进阶之STL》【哈希表】

《C++进阶之STL》【哈希表】

【哈希表】目录 * 前言 * ------------概念介绍------------ * 1. 什么是哈希? * ------------核心术语------------ * 一、哈希函数 * 1. 哈希函数的核心特点是什么? * 2. 哈希函数的设计目标是什么? * 3. 常见的哈希函数有哪些? * 直接定址法 * 除法散列法 * 乘法散列法 * 全域散列法 * 二、负载因子 * 1. 什么是负载因子? * 2. 负载因子对哈希表的性能有什么影响? * 3. 负载因子超过阈值时会发什么? * 三、哈希冲突 * 四、冲突处理 * 方法一:开放定址法 * 线性探测 * 二次探测 * 双重散列 * 方法二:链地址法 * ------------基本操作------------ * 怎么解决键key不能取模的问题? * 一、开放定址法 * 哈希结构 * 删除操作 * 扩容操作 * 二、链地址法 * 哈希结构 *

By Ne0inhk
计算机毕业设计java基于JAVA的校园网络跳蚤市场系统的设计与应用 基于B/S架构的校园二手物品在线交易平台设计与实现 面向高校师生的闲置商品发布、检索与订单管理系统开发

计算机毕业设计java基于JAVA的校园网络跳蚤市场系统的设计与应用 基于B/S架构的校园二手物品在线交易平台设计与实现 面向高校师生的闲置商品发布、检索与订单管理系统开发

计算机毕业设计java基于JAVA的校园网络跳蚤市场系统的设计与应用i5l6k9 (配套有源码 程序 mysql数据库 论文) 本套源码可以在文本联xi,先看具体系统功能演示视频领取,可分享源码参考。 随着高校招生规模的不断扩大和学生消费水平的提高,校园内闲置物品数量日益增多,二手交易需求十分旺盛。然而,传统的校园二手交易多依赖于线下跳蚤市场、QQ群发布、校园公告栏等方式,存在信息分散、匹配效率低、交易流程不规范、价格不透明、缺乏信用保障等问题,难以满足广大师生便捷、安全、高效的二手物品交易需求。基于JAVA的校园网络跳蚤市场系统应运而生,它通过互联网技术将商品分类、二手商品发布、购物车、订单处理、收藏功能、通知公告等功能进行数字化整合,为买卖双方提供了一个高效、透明、可信的校园二手物品交易平台。该系统不仅促进了闲置资源的循环利用,也为学生创造了更加便捷、经济的购物体验,成为绿色校园建设和循环经济的重要实践。 系统核心功能概览: * 用户注册与登录:支持用户、管理员两类角色的注册与登录。 * 个人中心:用户可查看和修改个人资料,如用户账号、姓名、头像、性别、联系方式、余额等

By Ne0inhk
C++ 虚函数与纯虚函数:多态的核心实现基石

C++ 虚函数与纯虚函数:多态的核心实现基石

C++ 虚函数与纯虚函数:多态的核心实现基石 💡 学习目标:深度理解虚函数与纯虚函数的本质区别,掌握虚函数表的底层原理,能够灵活运用二者设计具备多态特性的类结构。 💡 学习重点:虚函数的声明与重写规则、纯虚函数与抽象类的使用场景、虚函数表的工作机制、虚函数的常见陷阱与解决方案。 一、虚函数的本质与定义 ✅ 结论:虚函数是 C++ 实现动态多态的核心,通过在基类成员函数前添加 virtual 关键字,允许派生类重写该函数,并在运行时根据对象的实际类型调用对应版本。 1.1 虚函数的声明语法 虚函数的声明必须在基类中进行,语法格式如下: class 基类名 {public:virtual 返回值类型 函数名(参数列表){// 函数体}}; 1.2 虚函数的核心特性 1. 运行时绑定:函数调用关系在程序运行时确定,而非编译时。 2. 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型完全一致(协变类型除外)。 3.

By Ne0inhk