Java并发基础:深度解析Reentrant可重入性实现

时间:2024-01-23 21:20:30

Java并发基础:深度解析Reentrant可重入性实现 - 程序员古德

内容摘要

可重入锁有助于避免死锁,因为它允许线程在不释放已持有的锁的情况下,重新进入同步代码块,在某些情况下是非常必要的,

什么是可重入性?

锁的可重入性(Reentrant)是指同一个线程可以多次获取同一个锁,而不会导致死锁或其他线程无法获取该锁的情况,可重入锁是一种特殊的锁,它允许一个线程在已经持有该锁的情况下,再次获取(或重入)该锁,而不会产生冲突或死锁。

这种机制是通过为每个锁关联一个持有者和一个计数器来实现的,当线程首次获取锁时,它成为锁的持有者,并且计数器设置为1。如果同一个线程再次获取该锁,计数器就会增加,每次线程释放锁时,计数器都会减少,只有当计数器归零时,其他线程才有机会获取该锁。

可重入锁有助于避免死锁,因为它允许线程在不释放已持有的锁的情况下,重新进入同步代码块,在某些情况下是非常必要的,例如,在递归函数中,或者在需要调用其他也使用相同锁的方法时。

Java中的ReentrantLock类就是一个可重入锁的实现,此外,synchronized关键字在Java中提供的内置锁也是可重入的。

代码案例

下面是一个简单的代码示例,演示了ReentrantLock可重入锁的特性。

这个示例包括一个Counter类,它使用ReentrantLock来保护对内部计数器的访问,以及一个客户端类Client,它调用Counter的方法来增加计数器的值,如下代码:

import java.util.concurrent.locks.ReentrantLock;  
 
/**
 * @创建人 程序员古德 <br>
 * @创建时间 2024/1/18 23:51 <br>
 * @修改人 暂无 <br>
 * @修改时间 暂无 <br>
 * @版本历史 暂无 <br>
 */

// Counter类使用ReentrantLock来保护计数器的状态  
public class Counter {  
    private final ReentrantLock lock = new ReentrantLock();  
    private int count = 0;  
  
    // 增加计数器的值  
    public void increment() {  
        lock.lock(); // 获取锁  
        try {  
            count++;  
            System.out.println("Counter incremented by " + Thread.currentThread().getName() + " to " + count);  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    // 递归增加计数器的值,演示可重入锁的特性  
    public void recursiveIncrement(int depth) {  
        if (depth <= 0) {  
            return;  
        }  
  
        lock.lock(); // 在递归调用中重复获取锁  
        try {  
            count++;  
            System.out.println("Counter recursively incremented by " + Thread.currentThread().getName() + " to " + count + " at depth " + depth);  
            recursiveIncrement(depth - 1); // 递归调用  
        } finally {  
            lock.unlock(); // 递归返回时释放锁  
        }  
    }  
}  
  
// 客户端类,用于调用Counter的方法  
public class Client implements Runnable {  
  
    private final Counter counter;  
  
    public Client(Counter counter) {  
        this.counter = counter;  
    }  
  
    @Override  
    public void run() {  
        // 调用increment方法  
        counter.increment();  
  
        // 调用recursiveIncrement方法进行递归增加  
        counter.recursiveIncrement(3);  
    }  
  
    public static void main(String[] args) {  
        Counter counter = new Counter();  
  
        // 创建并启动两个客户端线程  
        Thread clientThread1 = new Thread(new Client(counter));  
        Thread clientThread2 = new Thread(new Client(counter));  
  
        clientThread1.start();  
        clientThread2.start();  
  
        // 注意:由于线程调度的不确定性,输出的顺序可能会有所不同  
    }  
}

在上面代码中,Counter类有一个受ReentrantLock保护的count变量,increment方法简单地增加计数器的值,而recursiveIncrement方法递归地增加计数器的值,演示了同一个线程可以多次获取同一个锁而不会导致死锁的情况。程序运行结果如下:

Counter incremented by Thread-0 to 1  
Counter recursively incremented by Thread-0 to 2 at depth 3  
Counter recursively incremented by Thread-0 to 3 at depth 2  
Counter recursively incremented by Thread-0 to 4 at depth 1  
Counter incremented by Thread-1 to 5  
Counter recursively incremented by Thread-1 to 6 at depth 3  
Counter recursively incremented by Thread-1 to 7 at depth 2  
Counter recursively incremented by Thread-1 to 8 at depth 1

这个输出显示了两个线程交替增加计数器的值,并且每个线程都能够递归地获取锁来增加计数器的值,而不会相互阻塞或导致死锁,这就是ReentrantLock可重入性的体现。

实现原理

Java并发基础:深度解析Reentrant可重入性实现 - 程序员古德

ReentrantLock的可重入性是通过其内部类Sync实现的,该类继承自AbstractQueuedSynchronizer(AQS),Sync有两个子类:NonfairSync和FairSync,分别表示非公平锁和公平锁,但它们在可重入性的实现上是相同的,可重入性的关键在于,当一个线程尝试获取锁时,如果锁已经被同一个线程持有,那么该线程可以再次获取锁而不会阻塞,这是通过在AQS中维护一个状态变量state和一个表示当前锁持有者的线程变量来实现的。

以下是ReentrantLock中与可重入性相关的关键代码段及其解释:

1、AQS的状态变量state:在AQS中,state变量用于表示同步状态,对于ReentrantLock,这个变量表示当前线程持有锁的重入次数。

2、Sync.tryAcquire(int acquires)方法:当线程尝试获取锁时,会调用此方法,它首先检查锁是否已经被当前线程持有,如果是,则增加重入计数,如果不是,则尝试获取锁。

如下代码案例:

protected final boolean tryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) {  
        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;  
}

代码解释:

  1. 如果state为0,表示锁未被持有,尝试通过CAS操作将其设置为acquires(通常为1),并将当前线程设置为锁的独占所有者。
  2. 如果当前线程已经是锁的独占所有者,则增加state的值,表示重入次数增加。
  3. 如果其他线程尝试获取锁,则返回false。

3、Sync.tryRelease(int releases)方法: 当线程释放锁时,会调用此方法,它减少重入计数,并在计数为0时释放锁,如下代码:

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

代码解释:

  1. 从state中减去releases(通常为1),表示减少重入次数。
  2. 检查当前线程是否是锁的独占所有者,如果不是,则抛出异常。
  3. 如果重入次数减至0,将锁的独占所有者设置为null,并标记free为true表示锁已被完全释放。
  4. 更新state的值。

关注我,每天学习互联网编程技术 - 程序员古德

END!