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

文章目录

    • 引言
    • 第一部分:重入锁基础概念
      • [1.1 什么是重入锁?](#1.1 什么是重入锁?)
      • [1.2 为什么需要重入锁?](#1.2 为什么需要重入锁?)
      • [1.3 ReentrantLock的基本用法](#1.3 ReentrantLock的基本用法)
    • 第二部分:ReentrantLock的核心特性
      • [2.1 可重入性](#2.1 可重入性)
      • [2.2 公平锁与非公平锁](#2.2 公平锁与非公平锁)
        • [2.2.1 概念解析](#2.2.1 概念解析)
        • [2.2.2 为什么默认非公平锁?](#2.2.2 为什么默认非公平锁?)
        • [2.2.3 源码层面的差异](#2.2.3 源码层面的差异)
      • [2.3 可中断锁](#2.3 可中断锁)
      • [2.4 限时等待锁](#2.4 限时等待锁)
      • [2.5 条件变量(Condition)](#2.5 条件变量(Condition))
    • 第三部分:ReentrantLock与synchronized的全面对比
      • [3.1 异同点总结](#3.1 异同点总结)
      • [3.2 如何选择?](#3.2 如何选择?)
    • 第四部分:ReentrantLock源码深度剖析
      • [4.1 AQS基础:重入锁的基石](#4.1 AQS基础:重入锁的基石)
        • [4.1.1 AQS的核心思想](#4.1.1 AQS的核心思想)
        • [4.1.2 AQS的关键方法](#4.1.2 AQS的关键方法)
      • [4.2 非公平锁源码解析](#4.2 非公平锁源码解析)
        • [4.2.1 加锁过程](#4.2.1 加锁过程)
        • [4.2.2 入队等待](#4.2.2 入队等待)
        • [4.2.3 释放锁](#4.2.3 释放锁)
      • [4.3 公平锁源码解析](#4.3 公平锁源码解析)
      • [4.4 限时获取锁的实现](#4.4 限时获取锁的实现)
    • 第五部分:CAS与AQS------重入锁的底层基石
      • [5.1 CAS操作](#5.1 CAS操作)
      • [5.2 CAS的ABA问题](#5.2 CAS的ABA问题)
      • [5.3 AQS的设计精髓](#5.3 AQS的设计精髓)
    • 第六部分:实战应用与最佳实践
      • [6.1 标准使用模板](#6.1 标准使用模板)
      • [6.2 监控与调试](#6.2 监控与调试)
      • [6.3 常见陷阱与注意事项](#6.3 常见陷阱与注意事项)
      • [6.4 性能考量](#6.4 性能考量)
    • 结语

引言

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

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


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

1.1 什么是重入锁?

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

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

java 复制代码
public class ReentrantExample {
    private final Object lock = new Object();
    
    public void methodA() {
        synchronized (lock) {
            // 已经持有锁
            methodB(); // 再次请求同一把锁
        }
    }
    
    public void methodB() {
        synchronized (lock) {
            // 这里不会死锁,因为synchronized是可重入的
            System.out.println("methodB执行");
        }
    }
}

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

1.2 为什么需要重入锁?

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

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

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

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

1.3 ReentrantLock的基本用法

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

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在finally中释放锁!
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

核心要点

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

第二部分:ReentrantLock的核心特性

2.1 可重入性

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

java 复制代码
public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void outer() {
        lock.lock();
        try {
            System.out.println("外层方法获取锁");
            inner();
        } finally {
            lock.unlock();
        }
    }
    
    public void inner() {
        lock.lock(); // 同一线程再次获取锁
        try {
            System.out.println("内层方法再次获取锁");
        } finally {
            lock.unlock();
        }
    }
}

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

2.2 公平锁与非公平锁

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

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

java 复制代码
ReentrantLock fairLock = new ReentrantLock(true);   // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
ReentrantLock defaultLock = new ReentrantLock();    // 默认非公平锁
2.2.2 为什么默认非公平锁?

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

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

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

java 复制代码
// NonfairSync的lock方法
final void lock() {
    // 直接尝试抢占锁(插队)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

而公平锁的lock()方法:

java 复制代码
// FairSync的lock方法
final void lock() {
    acquire(1); // 直接进入队列,没有抢占机会
}

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

java 复制代码
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平锁的额外判断:队列中是否有前驱节点
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ... 重入逻辑
    return false;
}

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

2.3 可中断锁

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

java 复制代码
public class InterruptibleDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void performTask() throws InterruptedException {
        // 可响应中断的锁获取
        lock.lockInterruptibly();
        try {
            // 执行需要同步的操作
            System.out.println(Thread.currentThread().getName() + " 获得锁");
            Thread.sleep(5000);
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws Exception {
        InterruptibleDemo demo = new InterruptibleDemo();
        
        Thread t1 = new Thread(() -> {
            try {
                demo.performTask();
            } catch (InterruptedException e) {
                System.out.println("线程1被中断");
            }
        });
        
        Thread t2 = new Thread(() -> {
            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提供了带超时的锁获取方法:

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimeoutDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public boolean tryExecute() {
        try {
            // 尝试在3秒内获取锁
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 获得锁");
                    Thread.sleep(2000); // 模拟业务操作
                    return true;
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 获取锁超时");
                return false;
            }
        } catch (InterruptedException e) {
            System.out.println("线程被中断");
            return false;
        }
    }
}

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

2.5 条件变量(Condition)

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

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    private final Object[] items = new Object[10];
    private int putIndex, takeIndex, count;
    
    public void put(Object x) throws InterruptedException {
        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();
        }
    }
    
    public Object take() throws InterruptedException {
        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 异同点总结

比较维度 synchronized ReentrantLock
实现方式 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 加锁过程
java 复制代码
// ReentrantLock.NonfairSync
final void lock() {
    // 第一步:直接尝试抢占锁(插队)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 抢占失败,进入AQS流程
}
  • compareAndSetState(0, 1):通过CAS尝试将state从0改为1。如果成功,表示当前线程直接抢到了锁,设置独占线程为当前线程。
  • 如果CAS失败(锁已被其他线程持有),调用acquire(1)进入AQS的获取流程。
java 复制代码
// AbstractQueuedSynchronizer.acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&          // 再次尝试获取(非公平版)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入等待队列
        selfInterrupt();
}

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

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // state为0,说明锁空闲,再次尝试CAS抢占
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入:同一线程再次获取锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

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

4.2.2 入队等待

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

java 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(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,在队列中自旋等待:

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node 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 释放锁
java 复制代码
// ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

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

4.3 公平锁源码解析

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

java 复制代码
// FairSync.lock
final void lock() {
    acquire(1);
}

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

java 复制代码
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平锁:检查队列中是否有前驱节点
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ... 重入逻辑
    return false;
}

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

4.4 限时获取锁的实现

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

java 复制代码
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    long lastTime = System.nanoTime();
    final Node node = addWaiter(Node.EXCLUSIVE);
    // ...
    for (;;) {
        // 尝试获取锁
        // 计算剩余时间,超时则返回false
        nanosTimeout -= System.nanoTime() - lastTime;
        if (nanosTimeout <= 0) {
            cancelAcquire(node);
            return false;
        }
        // 如果超时时间短,自旋;否则挂起
        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 标准使用模板

java 复制代码
public class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    
    // 带超时的获取
    public boolean tryIncrement(long timeout, TimeUnit unit) {
        try {
            if (lock.tryLock(timeout, unit)) {
                try {
                    count++;
                    return true;
                } finally {
                    lock.unlock();
                }
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

6.2 监控与调试

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

java 复制代码
ReentrantLock lock = new ReentrantLock();

// 查询锁状态
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并发编程思想的重要一步。在实际开发中,根据场景选择合适的同步工具,平衡功能与性能,才能写出高质量的多线程程序。

相关推荐
知识即是力量ol1 小时前
口语八股—— Spring 面试实战指南(终篇):常用注解篇、Spring中的设计模式
java·spring·设计模式·面试·八股·常用注解
Electron-er2 小时前
深入解析C语言memcmp函数:内存比较的利器与陷阱(附实战案例)
c语言·开发语言
m0_531237172 小时前
C语言-操作符
c语言·开发语言
yuezhilangniao2 小时前
win10环境变量完全指南:Java、Maven、Android、Flutter -含我的环境备份
android·java·maven
追随者永远是胜利者2 小时前
(LeetCode-Hot100)32. 最长有效括号
java·算法·leetcode·职场和发展·go
lifallen2 小时前
CDQ 分治 (CDQ Divide and Conquer)
java·数据结构·算法
清水白石0082 小时前
Python 性能分析实战指南:timeit、cProfile、line_profiler 从入门到精通
开发语言·python
笨蛋不要掉眼泪2 小时前
OpenFeign远程调用详解:声明式实现、第三方API集成与负载均衡对比
java·运维·负载均衡
月光有害2 小时前
深入解析批归一化 (Batch Normalization): 稳定并加速深度学习的基石
开发语言·深度学习·batch