Java并发编程--JUC并发工具类之ReentrantLock

摘要

  • 本文介绍ReentrantLock相关技术

  • 本文基于jdk1.8

ReentrantLock

  • ReentrantLock 是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。

  • 它的功能类似于 synchronized,是一种互斥锁,可以保证线程安全。

  • 相对于 synchronized,ReentrantLock 具备如下特点:

    • 可中断
    • 可以设置超时时间
    • 可以设置为公平锁
    • 支持多个条件变量
    • 与 synchronized 一样,都支持可重入
  • 它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性。

常用API

方法 描述
void lock() 获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
void lockInterruptibly() throws InterruptedException 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 超时获取锁,当前线程在以下三种情况下会被返回:
1. 当前线程在超时时间内获取了锁,返回true
2. 当前线程在超时时间内被中断,抛出InterruptedException
3. 超时时间结束,返回false
void unlock() 释放锁
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ReentrantLock lock = new ReentrantLock();//默认非公平锁

ReentrantLock lock = new ReentrantLock(true);//公平锁

// 加锁 阻塞
lock.lock();
try {
// ...
} finally {
// 解锁
lock.unlock();
}

// 尝试加锁 非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// ...
} finally {
lock.unlock();
}
}
  • 在使用时要注意 4 个问题:

      1. 默认情况下 ReentrantLock 为非公平锁而非公平锁;
      1. 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
      1. 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
      1. 释放锁一定要放在 finally 中,否则会导致线程阻塞。

小贴士

公平锁和非公平锁

  • 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
  • 非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。

可重入锁

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

  • Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

  • 在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。

ReentrantLock的等待通知机制

  • java.util.concurrent类库中提供Condition类来实现线程之间的协调。

  • 调用Condition.await() 方法使线程等待,同时释放锁

  • 其他线程调用Condition.signal()Condition.signalAll() 方法唤醒等待的线程。

  • 注意:调用Conditionawait()signal()方法,都必须在lock保护之内。

  • 下面是一个使用ReentrantLock的生产者–消费者模型的示例代码,在这个示例程序中,我们使用了一个ReentrantLock和两个Condition(notFull和notEmpty)来实现等待通知机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerExample {
private static final int CAPACITY = 10; // 队列容量
private static Queue<Integer> queue = new LinkedList<>(); // 共享队列
private static ReentrantLock lock = new ReentrantLock(); // 可重入锁
private static Condition notFull = lock.newCondition(); // 非满条件
private static Condition notEmpty = lock.newCondition(); // 非空条件

public static void main(String[] args) {
Thread producerThread = new Thread(new Producer()); // 创建生产者线程
Thread consumerThread = new Thread(new Consumer()); // 创建消费者线程
producerThread.start(); // 启动生产者线程
consumerThread.start(); // 启动消费者线程
}

static class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
lock.lock(); // 获取锁
try {
while (queue.size() == CAPACITY) {
// 队列已满,等待非满条件
notFull.await();
}
queue.offer(i); // 将项目放入队列
System.out.println("生产者生产: " + i);
// 通知消费者队列非空
notEmpty.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁
}
}
}
}

static class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
lock.lock(); // 获取锁
try {
while (queue.isEmpty()) {
// 队列为空,等待非空条件
notEmpty.await();
}
int item = queue.poll(); // 从队列中取出项目
System.out.println("消费者消费: " + item);
// 通知生产者队列非满
notFull.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁
}
}
}
}
}
  • 对于Conditionawait()signal()方法,它们的行为与使用wait()notify()方法时的情况类似。具体而言:

    • Conditionawait()方法会释放当前线程持有的锁,并使线程进入等待状态,直到接收到signal()方法的通知后才会重新竞争锁并继续执行。
    • Conditionsignal()方法会发送一个通知给等待在该条件上的一个线程,使其从等待状态被唤醒。注意,signal()方法执行后,并不会立即释放锁,它会等待当前线程执行完临界区代码后才会释放锁,然后等待被唤醒的线程重新竞争锁。

ReentrantLock具体应用场景

  • 1.解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。

  • 2.实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。

  • 3.实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。

ReentrantLock的问题

  • ReentrantLock 是一个互斥锁,同一时间只允许一个线程持有锁,其他线程必须等待释放锁后才能获取锁,适用于那些读操作少、写操作多的场景,因为读操作时其他线程无法读取,导致并发性能较低。

  • ReentrantLock 可以是公平锁(fairness=true)或非公平锁(fairness=false)。在公平锁模式下,锁将按照线程请求的顺序分配,但会导致性能下降。在非公平锁模式下,线程有机会插队获取锁,可能导致某些线程长时间等待。