专业的编程技术博客社区

网站首页 > 博客文章 正文

ReentrantLock原理及详细的使用方法

baijin 2024-10-16 07:37:23 博客文章 16 ℃ 0 评论

ReentrantLock 是 Java 并发编程中一种重要的同步机制,它比传统的 synchronized 提供了更高的灵活性和功能。下面将从 ReentrantLock 的基本原理、详细使用方法、内部实现机制、注意事项等方面详细说明。

一、ReentrantLock的基本原理

ReentrantLock 是基于可重入的概念设计的锁。当一个线程已经获取了 ReentrantLock 锁,它可以再次进入该锁的同步代码块而不会陷入死锁。这是因为 ReentrantLock 记录了每个线程获取锁的次数,并允许同一线程多次获取它。线程获取锁的次数与释放锁的次数必须匹配,才能真正释放锁。

1.1 AQS(AbstractQueuedSynchronizer)

ReentrantLock 的核心是基于 AQS(抽象队列同步器)实现的。AQS 维护了一个队列,用于管理所有请求锁的线程。每个线程尝试获取锁时,如果当前锁被其他线程持有,则进入等待队列,阻塞等待锁释放。一旦锁释放,AQS 会从队列中唤醒下一个线程,允许它获取锁。

1.2 可重入性

可重入锁允许线程多次进入同一把锁。比如,一个递归方法可以在方法调用链中多次获取同一个锁而不导致死锁。这种特性主要依赖于线程独有的锁计数器。

1.3 公平锁与非公平锁

  • 公平锁:按照线程请求锁的顺序进行排队,先到先得。公平锁避免了线程“饥饿”,但性能可能比非公平锁稍低。
  • 非公平锁:线程直接尝试获取锁,不关心等待队列中的其他线程,可能会导致某些线程长期等待。非公平锁性能较高,因为它减少了上下文切换和线程调度的开销。

二、ReentrantLock的详细使用方法

ReentrantLock 提供了比 synchronized 更多的控制能力,如尝试获取锁、可中断锁等待、锁的公平性等。下面逐一介绍其主要用法。

2.1 基本使用

在使用 ReentrantLock 时,通常遵循以下步骤:

  1. 创建锁对象:通过 new ReentrantLock() 创建锁实例。
  2. 获取锁:使用 lock() 或其他加锁方法获取锁。
  3. 执行临界区代码:在持有锁的情况下,执行需要保护的共享资源代码。
  4. 释放锁:使用 unlock() 释放锁,确保其他线程能够继续执行。
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    private final ReentrantLock lock = new ReentrantLock(); // 创建锁
    private int counter = 0; // 共享资源

    public void increment() {
        lock.lock(); // 获取锁
        try {
            counter++;
            System.out.println(Thread.currentThread().getName() + " - Counter: " + counter);
        } finally {
            lock.unlock(); // 确保在 finally 中释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        
        Thread t1 = new Thread(demo::increment);
        Thread t2 = new Thread(demo::increment);
        
        t1.start();
        t2.start();
    }
}

2.2 尝试获取锁

使用 tryLock() 可以尝试在不阻塞的情况下获取锁。如果当前锁未被持有,则立即获取并返回 true,否则返回 false。这种方法适合用于避免死锁。

if (lock.tryLock()) {
    try {
        // 执行临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 锁不可用时的处理逻辑
}

还可以设置超时时间来获取锁,避免线程长时间等待锁:

try {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            // 获取锁后执行的逻辑
        } finally {
            lock.unlock();
        }
    } else {
        // 超时未能获取锁
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

2.3 可中断锁

lockInterruptibly() 允许线程在等待锁时响应中断。这对于实现中断操作的灵活性非常有用。

try {
    lock.lockInterruptibly(); // 可中断的锁获取
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 处理中断
    e.printStackTrace();
}

2.4 公平锁与非公平锁

通过构造函数选择锁的公平性:

  • 非公平锁(默认)
  • ReentrantLock lock = new ReentrantLock();
  • 公平锁
  • ReentrantLock lock = new ReentrantLock(true);

公平锁保证线程按顺序获取锁,但性能较低;非公平锁可能导致“饥饿”,但性能更高。

三、ReentrantLock的内部实现

ReentrantLock 基于 AQS 实现,它通过原子操作 CAS 来控制同步状态。AQS 维护了两个核心状态:

  1. state:表示锁的状态,0 表示未锁定,1 表示锁定。
  2. 线程队列:AQS 使用一个 FIFO 队列来管理等待锁的线程。

当线程尝试获取锁时:

  1. 如果 state == 0,线程成功获取锁,并将 state 设置为 1。
  2. 如果锁已经被持有(state != 0),线程进入等待队列,直到锁释放后被唤醒。

当持有锁的线程调用 unlock() 时,state 被递减。只有当 state 递减到 0 时,锁才真正释放。

四、使用 ReentrantLock的注意事项

4.1 必须手动释放锁

ReentrantLocksynchronized 的主要区别之一是锁的释放必须显式调用 unlock()。忘记释放锁会导致其他线程永远无法获取锁。因此,通常在 try 块中获取锁,在 finally 块中释放锁,以确保异常情况下锁也能被正确释放。

4.2 避免死锁

当多个线程持有多个锁时,容易产生死锁问题。为了避免死锁,可以采用锁的超时机制,或者设计更合理的加锁顺序。

4.3 性能与公平性

非公平锁的性能比公平锁更好,但可能导致某些线程长期无法获得锁。公平锁避免了这种情况,但每次加锁和解锁的性能稍差。在高并发场景下,通常选择非公平锁来提升性能。

4.4 避免锁竞争

过度使用锁可能导致线程竞争和性能下降。在设计多线程应用时,尽量减少临界区的长度,避免不必要的锁。

五、总结

ReentrantLock 提供了比 synchronized 更灵活的锁控制方式。它可以:

  • 选择公平锁和非公平锁;
  • 提供尝试获取锁和可中断的锁;
  • 允许锁的重入;
  • 支持手动加锁和解锁的细粒度控制。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表