java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java重入锁ReentrantLock

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

作者:百锦再@新空间创想科技

本文给大家介绍Java重入锁(ReentrantLock)从入门到源码深度解析,通过本文学习可以全方位地认识ReentrantLock,从基本概念到高级特性,从使用方式到源码剖析,从底层原理到实际应用,感兴趣的朋友跟随小编一起看看吧

引言

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

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

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

1.1 什么是重入锁?

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

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

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的几个功能性缺陷

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

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

1.3 ReentrantLock的基本用法

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

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();
        }
    }
}

核心要点

第二部分:ReentrantLock的核心特性

2.1 可重入性

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

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 概念解析

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

ReentrantLock fairLock = new ReentrantLock(true);   // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
ReentrantLock defaultLock = new ReentrantLock();    // 默认非公平锁

2.2.2 为什么默认非公平锁?

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

2.2.3 源码层面的差异

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

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

而公平锁的lock()方法:

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

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

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提供了可中断的获取锁方式:

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提供了带超时的锁获取方法:

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()方法分解为不同的条件对象,使得一个锁可以支持多个等待集,实现更精细的线程协作。

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 异同点总结

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

3.2 如何选择?

根据实际场景选择:

第四部分: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.NonfairSync
final void lock() {
    // 第一步:直接尝试抢占锁(插队)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 抢占失败,进入AQS流程
}
// AbstractQueuedSynchronizer.acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&          // 再次尝试获取(非公平版)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入等待队列
        selfInterrupt();
}

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

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加入等待队列尾部:

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,在队列中自旋等待:

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 释放锁

// 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),没有抢占尝试:

// FairSync.lock
final void lock() {
    acquire(1);
}

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

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实现:

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时,才将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 标准使用模板

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提供了监控锁状态的方法:

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 性能考量

结语

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

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

到此这篇关于Java重入锁(ReentrantLock)全面解析:从入门到源码深度剖析的文章就介绍到这了,更多相关Java重入锁ReentrantLock内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文